雖然現代化的 web 開發更多地依賴各種 MVC 框架,但開發者仍需要熟練掌握 HTML 與 DOM 方面的基礎知識。不過,即使是有着多年經驗的前端開發者,也會遇到一些不明所以的情況。本文首先將爲初學者介紹 HTML 與 DOM 的基本常識,隨後爲大家介紹15個比較冷門的 HTML 元素的方法。
初學者
首先讓我們來討論一下 HTML 與 DOM 之間的區別。
顯然,普通的 <table> 元素就是一段 HTML 代碼,它可以應用在任何一個以 .html 爲擴展名的文件中。元素自帶一系列特性(attribute),以控制它的顯示方式與行爲。
這就是元素的全部內容,它與 JavaScript 沒有任何關聯。
而 DOM 的作用是將你的 JavaScript 代碼與文檔中的 HTML 元素關聯在一起,讓你能夠以對象的方式與這些元素進行交互。
這就是所謂的文檔對象模型。
在 HTML 中的每個元素都對應着一個 DOM ‘接口’,其中定義了若干屬性(property,通常會映射至 HTML 元素上的特性)與方法。舉例來說,一個 <table> 元素對應着一個 HTMLTableElement 接口。
你可以按以下方式獲取某個元素的引用:
const searchBox = document.getElementById('search-box');
現在,你就可以訪問該元素上定義的所有屬性與方法了。打個比方,你可以通過 searchBox.value 訪問它的 value 屬性,也可以調用 searchBox.focus() 方法讓光標移至輸入框上。
感謝你參加這個58秒的 DOM 入門培訓課程,哈哈。
現在的問題在於,大多數元素都沒有提供什麼有趣的方法。因此,除非你特意到官方文檔規範上去搜索那些可能永遠都用不到的東西,否則很容易忽略掉那些零散的小技巧。
幸運的是,瀏覽規範與整理小技巧正是我用於避免陷入困境的兩種最喜歡的方式。那麼,讓我們開始吧……
如果你也希望嘗試一下這些技巧,又恰好有一些瀏覽器 DevTools 可以使用的話,可以在元素樹型結構中先選中某個元素,然後在控制檯中輸入 $0,它會返回給你一個所選中元素的引用。如果你需要返回該元素的對象,請輸入 dir($0)。
1 table 的方法
原始的 table 元素(時至今日仍然是網站佈局方法裏的第一名)本身自帶許多精巧的方法,使用這些方法創建表格就像搭建宜家裏的桌子一樣簡單。
以下是部分實用的方法。
const tableEl = document.querySelector('table');
const headRow = tableEl.createHead().insertRow();
headerRow.insertCell().textContent = 'Make';
headerRow.insertCell().textContent = 'Model';
headerRow.insertCell().textContent = 'Color';
const newRow = tableEl.insertRow();
newRow.insertCell().textContent = 'Yes';
newRow.insertCell().textContent = 'No';
newRow.insertCell().textContent = 'Thank you';
整段代碼裏完全用不着使用 document.createElement() 方法。
如果你在一個 table 元素上直接調用 .insertRow() 方法,它甚至會自動爲你插入一個 <tbody> 元素,是不是很棒?
2 scrollIntoView()
你知道嗎?當頁面的 URL 中包含 #something 元素時,一旦頁面加載,瀏覽器就會自動滾動至具有這個 ID 的元素之處。
這確實是一項很貼心的功能,但如果你在頁面加載之後再渲染元素,這項功能就不起作用了。
不過,你也可以通過以下方式,手動地讓這項功能重新生效:
document.querySelector(document.location.hash).scrollIntoView();
3 hidden
好吧,hidden 或許不是一個方法,但如果你提出抗議,那我也要爭論一下:在 hidden 的背後很可能對應着一個 setter,這可是一個貨真價實的方法,對不對?
不管怎樣,你是否曾經爲了隱藏某個元素而使用過 myElement.style.display = 'none' 這種方法呢?如果是的話,請別再這麼做了!
只需調用 myElement.hidden = true ,即可實現元素隱藏的功能。
4 toggle()
嗯,toggle 也不算是元素的方法,它實際上是元素屬性上的一個方法。嚴格來說,這是一種爲元素添加或刪除某個 class 的方法,具體做法是 myElement.classList.toggle('some-class') 。
如果你曾經通過 if 條件語句爲元素添加 class,那就應該趕緊改用這種做法。
正確的方式是爲 toggle 方法傳入第二個參數,如果該參數返回 true ,則指定的 class 就會添加至元素上。
el.classList.toggle('some-orange-class', theme === 'orange');
我知道你在想些什麼:這種寫法違背了 ‘toggle’ 這個詞的本義(開關),那些從 IE 時代過來的開發者們都這麼想,他們斷言應當徹底摒棄使用第二個參數的做法。
所以,我收回我的話。不必堅持這種寫法了,各位請隨意!
5 querySelector()
好吧,你當然知道這個方法,但據我推測,應該只有 17% 的開發者才知道,該方法可以使用在任意元素上。
打個比方,myElement.querySelector('.my-class') 的作用是返回在 myElement 的子代中包含 my-class 這個 class 的所有元素。
6 closest
該方法可在任意元素上使用,它能夠向上查找元素的樹型結構,可以理解爲 與 querySelector() 相反的方法。因此,我可以通過以下方法獲取當前內容的對應標頭:
myElement.closest('article').querySelector('h1');
這段方法首先向上找到最近的 <article> 元素,然後再向下找到最近的 <h1> 元素。
7 getBoundingClientRect()
在對 DOM 元素調用該方法時,將返回一個包含其空間結構詳細信息的簡單對象。
{
x: 604.875,
y: 1312,
width: 701.625,
height: 31,
top: 1312,
right: 1306.5,
bottom: 1343,
left: 604.875
}
不過,在調用該方法時需要注意兩點:
調用該方法會導致元素的重繪,根據設備與頁面複雜程度的不同,重繪的時間可能會佔用幾毫秒。因此,如果你需要重複地調用該方法,例如在使用動畫的場景下,需要特別注意這一點。
並非所有的瀏覽器都會返回這些值,他們有這個責任麼?
8 matches()
假設我需要檢查某個元素是否包括一個特定的 class。
這是最複雜的方式:
if (myElement.className.indexOf('some-class') > -1) {
// do something
}
比上面的好一點,但和本文沒什麼關係:
if (myElement.className.includes('some-class')) {
// do something
}
最佳方式:
if (myElement.matches('.some-class')) {
// do something
}
9 insertAdjacentElement()
我今天才剛學到這一條!它的作用類似於 appendChild()
,但能夠更好地控制插入子元素的具體位置。
parentEl.insertAdjacentElement('beforeend', newEl) 與 parentEl.appendChild(newEl) 的作用是一樣的,但除此之外,你還可以指定 beforebegin、afterbegin 或 afterend 這幾個參數值,元素將按這些值的名稱所示插入相應的位置。
多麼強大的控制能力!
多棒的點子。
10 contains()
你有沒有遇到過這樣的情形,需要知道某個元素是否被包含在另一個元素中?至少我本人經常會遇到這樣的問題。
打個比方,假設我在處理一個鼠標點擊事件時,需要知道它是發生在一個模態窗口中還是發生在外面(這樣我才能夠關閉這個窗口),我大概會這麼做:
const handleClick = e => {
if (!modalEl.contains(e.target)) modalEl.hidden = true;
};
代碼中的 modalEl 是模態窗口的引用,而 e.target 則代表各種發生點擊事件的元素。
有趣的是,每當遇到這種情形,在我第一遍寫代碼的時候,100%的概率會將其中的判斷邏輯寫反。
哪怕是我提高了警惕,並在保存代碼之前嘗試將邏輯顛倒過來寫,仍然還是寫錯。
11 getAttribute()
這毫無疑問是所有元素方法中最沒用的一個,但有一個場景除外。
你是否記得,我在本文的開頭部分曾提到,對象的屬性 property 通常也會映射到它的特性 attribute 中(我在上文中特別用粗體強調了這一點,注意不是斜體)?
但在某一個場景中,這種假設並不成立,這就是某個元素的 href 特性,例如
<a href="/animals/cat">Cat</a> 。
調用 el.href 不會返回 /animals/cat,這可能與你的猜測不符。原因在於 元素實現了 HTMLHyperlinkElementUtils接口,該接口提供了一系列輔助屬性,例如 prototol 與 hash 等等,以展現與鏈接的目標相關的值。
href 就是其中一個實用的屬性,它將返回完整的 URL,並去掉無用的空格,而不是返回在特性中所指定的相對 URL。
這樣一來,如果你需要獲取 href 特性中的字符串字面值,就只能使用 el.getAttribute('href') 方法了。
12 dialog 元素的三大法寶
<dialog> 是一個相對較新的元素,它帶來了兩個還算能用的方法,和一個非常棒的方法。其中show() 和 close() 方法的功能與你所想象的一樣,我感覺還算可以。
而 showModal() 方法能夠將 <dialog> 元素顯示在頁面的頂層,居中對齊,這正是所期望的模態窗口行爲。你無需指定 z-index,或者手動添加一個灰色的背景,也不需要監聽 esc 按鍵以關閉此窗口。瀏覽器能夠理解模態窗口的工作方式,並自動完成你所期望的行爲。
這真是太棒了。
13 forEach()
某些情況下,當你獲取到一個元素列表的引用時,可以通過 forEach() 方法進行迭代式調用。
用 for() 進行循環已經是 2014 年代的老古董了。
假設你需要記錄頁面中所有鏈接的 URL,可以輸入以下代碼,只要你不介意看到報錯。
document.getElementsByTagName('a').forEach(el ==> {
console.log(el.href);
});
也可以這麼做:
document.querySelectorAll('a').forEach(el ==> {
console.log(el.href);
});
問題出在 getElementsByTagName 與其他類似的 get… 方法返回的是一個 HTMLCollection 接口,而 querySelectorAll返回的是一個 NodeList 接口。
而 NodeList 接口爲我們提供了 forEach() 方法(此外還包括 keys()、values(),和 entries() 等方法 )。
理想的情況下,最好是每個方法都只返回簡單的數組,而不是返回一些類似數組的對象。不過別擔心,ECMA 大神爲我們提供了 Array.from() 方法,它能夠把所有這些類數組對象轉化爲一個真正的數組。
所以,這樣的代碼就能夠正常工作:
Array.from(document.getElementsByTagName('a')).forEach(el ==> {
console.log(el.href);
});
獎勵關卡:創建了一個數組之後,你就能夠對其使用 map() 、filter() 和 reduce() 以及其他各種數組方法了。打個比方,先不管目的是什麼,總之你可以按以下方式返回所有外部鏈接的數組:
Array.from(document.querySelectorAll('a'))
.map(el => el.origin)
.filter(origin => origin !== document.origin)
.filter(Boolean);
我最喜歡的一個方法是 .filter(Boolean),它肯定會給將來的我在調試問題時帶來無窮的煩惱,哈哈。
14 表單
或許你已經知道,<form> 有一個 submit() 方法。但或許你不知道表單還有一個 reset() 方法,而且當你需要對錶單元素進行驗證時,還可以調用 reportValidity() 方法。
此外,你也可以通過對錶單的 elements 屬性加上元素的 name 特性 的方式調用它的屬性。打個比方,myFormEl.elements.email 將返回屬於某個 <form> 中的 <input name="email" /> 元素(‘屬於’,並不代表它一定是一個‘子元素’)。
好吧,其實剛纔我是騙你的。elements 並不會返回一個元素列表,而是返回一個控件列表(顯然它不是一個數組,因爲沒必要這麼做)。
舉例來說:假設你有三個單選按鈕,每個都有相同的名稱 animal,那麼 formEl.elements.animal 將返回一個單選按鈕集的引用(一個控件,三個元素)。
而 formEl.elements.animal.value 將返回所選中的單選按鈕的值。
這種語法看起來非常古怪,讓我們來分解一下看看:formEl 是一個元素,elements 則對應 HTMLFormControlsCollection 接口,這並非一個真正的數組,其中的每一項內容也未必代表一個 HTML 元素。animal是多個單選按鈕的集合,只是因爲他們具有相同的 name 特性才聚集在一起(RadioNodeList 接口就是爲此而生的),而 value 則返回該集合中所選中的那個單選按鈕的 value 特性。
非常直觀,嗯……
15 select()