Window 及 Document
window 是指瀏覽器視窗(最上層的全域物件),包含了 DOM。支援分頁的瀏覽器中,每一個分頁標籤都擁有自己的 window 物件。其本身有許多屬性跟方法。例如:
// 跳出提示視窗
window.alert('Hello world')
// 延遲某段時間(單位為毫秒)後,再執行一次 callback
window.setTimeout(callback, 5000)
// 固定延遲了某段時間,不斷循環執行 callback
windwo.setInterval(callback, 5000)
// 開啟一個新視窗
window.open()
// 頁面載入時觸發
window.onload()
// 歷史列表
window.history
// 視窗中載入的 Document 物件
window.document
// 記錄瀏覽器 (broswer) 相關資訊
window.navigator
window.navigator 屬性如下:
屬性 | 值 |
---|---|
appCodeName | 設定的 code 名稱 |
appName | 使用瀏覽器的官方名稱 |
appVersion | 使用瀏覽器的版本 |
buildID | 使用瀏覽器的 ID,ID 的格式為 “YYYYMMDDHH” |
cookieEnabled | 瀏覽器是否可以寫入 cookie |
language | 瀏覽器的語系編碼 |
onLine | 瀏覽器是否有連線 |
oscpu | 作業系統及 CPU 名稱 |
platform | 平台(作業系統)名稱 |
plugins | 所有瀏覽器安裝的附加元件 |
… | … |
ps.使用時前面 window 可省略不寫
document 是指當前的 HTML 頁面。而 DOM (Document Object Model) 文件物件模型,是瀏覽器將 HTML 的標籤元素轉換成 JavaScript 可操作的物件。DOM 中的每個 HTML 元素就是一個節點。而 document 是根節點。
取得 DOM 元素
取得單一元素
document.getElementById('id')
const el = document.querySelector('#id')
or
const el = document.querySelector('.class')
取得多個元素
// 取得一整個 input 陣列的值,取值要加陣列索引
const inputElement = document.getElementsByTagName('div');
// 取得所有符合的元素節點列表(非陣列)
const allEl = document.querySelectorAll('.class')
// 轉成陣列後,就可用陣列方法
Array.from(allEl).foreach(element => { ... })
// 動態插入一個新的 divconst el = document.createElement('div')
document.body.appendChild(el)
ps. querySelector() 不能查詢動態更新的元素,要用 getElement 的方法
屬性(properties、attributes)處理
// 檢查是否有該屬性
document.querySelector('a').hasAttribute('href');
// 取得屬性值
document.querySelector('a').getAttribute('href');
// 設定屬性
document.querySelector('a').setAttribute('href', 'Link');
ps.這些方式會讓瀏覽器進行重繪(redraw),拖慢效能。除非必要再使用。改用以下方式來處理
// 取得屬性
const value = el.value
// 設定屬性,直接指定
el.value = 'hi'
// 設定多個,用 Object.assign()
Object.assign(el.{ value: 'hello', id: 'world' })
// 刪除屬性
el.value = null
ps.目前大多都直接將屬性集合寫在 class 類別再使用。
類別(class)處理
// 增加類別,可多個
el.classList.add('abc','abc1')
// 移除類別,可多個
el.classList.remove('abc')
// 切換類別,元素上沒有這類別時,就新增類別;如果元素已經有了這類別,就刪除。
el.classList.toggle('abc')
// 檢查是否有此類別
el.classList.contains('abc')
// 查詢類別數量
el.classList.length
// 取得所有類別
const all = el.classList
// 修改類別(有 - 用[ ]語法)
el.style.background = 'gray';
el.style['background-color'] = '#000';
// 直接讀寫類別
el.style.cssText = 'font-size: 16x; color: gray;';
// 取得屬性值
window.getComputedStyle(el).getPropertyValue('font-size')
DOM 處理
// 建立元素節點
const divEl = document.createElement('div(tagName)')
// 建立文字節點
const TextNode = document.createTextNode('hello world')
// 在 el 元素的最後新增 el2 元素
el.appendChild(el2)
// NODE.insertBefore(newNode, refNode),將新節點 newNode 插入至指定的 refNode 節點的前面
el.insertBefore(el2, el3)
// 在 el 元素裡的 el3 元素之後新增 el2 元素
el.insertBefore(el2, el3.nextSibling)
// 替換,將 el 內的 oldNode 替換成 newNode
el.replaceChild(newNode, oldNode);
// 複製
const cloneEl = el.cloneNode()
// 移除(需要從父元素移除)
parentEl.removeChild(el)
// 移除本身元素
el.parentNode.removeChild(el)
// 修改元素的內容(包含子元素內容)
el.innerHTML = '<div><h1>hello world</h1></div>'
// 可增進效能的 document.createDocumentFragment(),DocumentFragment 不是真實的 DOM 結構,不會影響目前文件,所以不會導致回流(reflow)
1 | const ul = document.getElementById("List"); |
事件
事件資訊 function(e)
event 資訊會放在 callback function 裡面的第一個參數,通常都是取名 event 或簡寫 e,可當成是物件,內有各種此事件的參數值。此 callback 執行的時機是事件觸發後。e.eventPhase 是表示事件在那個階段(Phase)被觸發,用數字來表示(1:CAPTURING 2:TARGET 3:BUBBLING)
e.g.
click 滑鼠點擊事件
- e.target // 被點擊到的元素
- e.screenX // 滑鼠離視窗左邊的距離
- e.screenY // 滑鼠離視窗上邊的距離
keydown 鍵盤按下按鍵
- e.key // 按鍵號碼
事件監聽
監控標籤元素發生某件事(例如被點擊…)之後要進行的動作,若要改變這個事件要在什麼階段觸發,可以在 el.addEventListener() 加上第三個參數(可不加,不加為預設),為 boolean 值(true:捕獲、false(預設):冒泡)。
// 單一元素監聽
<button id="btn" onclick="console.log('Hello');">Click</button>
ps.不建議用上述方式綁定(框架例外)
1 | el.addEventListener('click', function (event) { |
//監聽多個元素,event.target 取得觸發目標元素
1 | Array.from(allEl).forEach(element => { |
// 解除事件監聽(註冊)
1 | el.removeEventListener('click', function(){ |
ps. 目前在 Vue & React 等網頁框架中,如果是使用內建的語法註冊事件監聽,它們都會自動在無用的時候移除,可以放心使用。但若是自己寫事件監聽,務必要記得移除。
傳遞機制(捕獲 CAPTURING 與冒泡 BUBBLING)
假設有一個列表,組成為 ul 跟 多個 li,當點了其中的一個 li。也相當於點擊了 ul。因為 li 被 ul 包住了。這樣一來 ul 及 li 的所監聽事件都將觸發。
事件傳遞順序是:
先捕獲:從根節點往下傳遞到 target,過程中觸發各別元素的捕獲階段事件監聽。
再冒泡:從 target 事件觸發後再逐層向上回到根節點過程中事件依序被觸發。
當事件傳到 target 本身時,沒有分捕獲跟冒泡。執行順序按照 addEventListener的順序而定,先添加的先執行,後添加的後執行。
取消事件傳遞 e.stopPropagation()
不會再把事件傳遞給上或下一個節點,看是捕獲跟冒泡機制。但若是同一個節點上,不只一個 listener 的話,還是會執行。
取消相同事件 e.stopImmediatePropagation()
當元素有綁定多個同樣的事件時,監聽將會按照註冊的先後順序被呼叫,若其中一個監聽事件呼叫了 e.stopImmediatePropagation(),將會阻止所有後面的綁定事件。
阻止預設動作 e.preventDefault()
取消瀏覽器的元素預設行為,跟事件傳遞無關。以下是比較常用的情況:
<form>
的 submit // 阻止送出表單<a>
的 click 事件 // 阻止轉址<input>
的 keypress 事件 // 阻止輸入按鍵
事件代理、委派(Event delegation)
一種利用Event Bubbling
,加上事件有兩個特別屬性:
target & currentTarget
target: 觸發的位置
currentTarget: 綁定處理器的位置(可加可不加)
而能減少監聽器數目的方法。
為了處理大量的事件監聽跟動態新增元素(appendChild)的事件監聽,可以透過事件傳遞的機制,將子元素事件監聽器交由父元素代理。
例如列表(ul、li)的監聽,可將監聽機制設定在 ul 上,而不是內部的每一個 li。如此一來不管 li 數量有多少,都可以利用事件傳遞機制(冒泡)來做事件監聽。
e.g.
1 | <div class="parent"> |
當點擊不同的小區塊時,就會 console 出個別的名字,例如:a、b 或c。
實作方法是將 click
事件綁在 parent 上,藉由Event Bubbling
來傳遞給 child,而非直接將事件綁定在 child 上。
優點:是可減少監聽器的數目。
缺點:是由於需要判斷哪些子元素是我們有興趣的項目,而必須多寫一些程式碼做判斷。
在上面的例子,我們加上一個filter 「.child」,表示只對有 「.child」節點有興趣,而沒有加上 「.child」的節點則不被影響,例如 click「.subitem」這個節點之後就不會 console 它的名字。
補充:window.onload
與 $(document).ready
的使用差異。可以參考 https://ithelp.ithome.com.tw/articles/10092601 此文章內容。基本上建議使用 $(document).ready
。
參考:
https://ithelp.ithome.com.tw/articles/10191867
https://www.fooish.com/javascript/dom/css.html
https://jmln.tw/blog/2017-07-07-vanilla-javascript-dom-manipulation.html
http://fstoke.me/blog/?p=2487
https://yakimhsu.com/project/project_w7_DOM.html
https://yakimhsu.com/projectproject_w7_eventListener.html
https://blog.techbridge.cc/2017/07/15/javascript-event-propagation/
https://medium.com/schaoss-blog/前端三十-07-js-瀏覽器-dom-元素的事件代理是指什麼-95f1e8f311db
https://medium.com/schaoss-blog/什麼捕獲冒泡-你是魚嗎-聊聊瀏覽器-dom-的事件傳遞-b44454690661
https://pydoing.blogspot.com/2011/10/javascript-window-navigator.html
https://ithelp.ithome.com.tw/articles/10191970
https://cythilya.github.io/2015/07/08/javascript-event-delegation/