天天看點

DOM 之通俗易懂講解

   DOM是所有前端開發每天打交道的東西,但是随着jQuery等庫的出現,大大簡化了DOM操作,導緻大家慢慢的“遺忘”了它的本來面貌。不過,要想深入學習前端知識,對DOM的了解是不可或缺的,是以本文力圖系統的講解下DOM的相關知識,如有遺漏或錯誤,還請大家指出一起讨論^ ^。

  一、DOM是什麼?

  DOM(文檔對象模型)是針對HTML和XML文檔的一個API,通過DOM可以去改變文檔。

  這個說法很官方,大家肯定還是不明白。

  舉個例子:我們有一段HTML,那麼如何通路第二層第一個節點呢,如何把最後一個節點移動到第一個節點上面去呢?

  DOM就是定義了如果做類似操作,那麼應該怎麼做的标準。比如用getElementById來通路節點,用insertBefore來插入節點。

  當浏覽器載入HTML時,會生成相應的DOM樹。

  簡而言之,DOM可以了解為一個通路或操作HTML各種标簽的實作标準。

  對于一個HTML來說,文檔節點Document(看不到的)是它的根節點,對應的對象便是document對象(嚴格講是子類HTMLDocument對象,下面單獨介紹Document類型時會指出)。

  換句話說存在一個文檔節點Document,然後它有子節點,比如通過document.getElementsByTagName("html"),得到類型為元素節點的Element html。

  每一段HTML标記都可以用相應的節點表示,例如:

  HTML元素通過元素節點表示,注釋通過注釋節點表示,文檔類型通過文檔類型節點表示等。

  一共定義了12種節點類型,而這些類型又都繼承自Node類型。

  是以我們首先講Node類型,因為這個類型的方法是所有節點都會繼承的。

  二、Node類型(基類,所有節點都繼承了它的方法)

  Node是所有節點的基類型,所有節點都繼承自它,是以所有節點都有一些共同的方法和屬性。

  先講Node類型的屬性

  首先是nodeType屬性,用來表明節點類型的,例如:

document.nodeType;    // 傳回 9 ,其中document對象為文檔節點Document的執行個體      

  這裡面,9代表的就是DOCUMENT_NODE節點的意思,可以通過Node.DOCUMENT_NODE檢視節點對應的數字

document.nodeType === Node.DOCUMENT_NODE;    // true      

  至于一共有哪些節點,每個節點對應的數字又是多少,這個可以問谷歌就知道了。反正常用的就是元素節點Element(對應數字為1)和文本節點Text(對應數字為3) 

  然後常用的還有nodeName和nodeValue

  對于元素節點 nodeName就是标簽名,nodeValue就是null

  對于文本節點 nodeName為"#text"(chrome裡面測試的),nodeValue就是實際的值

  每個節點還有childNodes屬性,這是個十分重要的屬性,它儲存了這個節點所有直接子元素

  調用childNodes傳回的是一個NodeList對象,它極其像數組,但是有一個最關鍵的地方,它是動态查詢的,也就是說每次調用它都會對DOM結構查詢,是以對它的使用需要慎重,注意性能。

  通路childNodes可以使用數組下表或者item方法

  然後各個節點還存在各種屬性讓它們可以互相通路,下圖很好的總結了

  比較有用的方法和屬性:

  1、hasChildNodes()

  如果包含子節點就傳回true,比查詢childNodes的length來的簡單。

  2、ownerDocument

  傳回文檔節點的引用(在html裡面也就是document對象)

  再介紹下Node類型常用的方法

  appendChild()方法可以在節點的childNodes的末尾增加一個節點,值得注意的是如果這個節點是已經存在在文檔中的,那麼便會删除原節點,感覺上就像是移動節點一樣。

  insertBefore()方法接受兩個參數,一個是插入的節點,另外一個是參照的節點。如果第二個參數為null,則insertBefore和appendChild效果一樣。否則便會把節點插入到參照節點之前。這裡要注意的是,如果第二個參數不為null,那麼插入的節點不能是已經存在的節點。

  replaceChild()方法可以替換節點,接受兩個參數,需要插入的節點和需要替換的節點。傳回被替換掉的節點。

  removeChild()移除節點。這裡有個常見需求,比如我有一個節點 #waste-node ,那麼如何移除它呢?

var wasteNode =  document.getElementById("waste-node");
wasteNode.parentNode.removeClhid(wasteNode);    // 先拿到父節點,再調用removeClild删除自己      

  這裡先暫停一下,不知道大家注意到沒有,以上的幾個方法都是操作某個節點的子節點,也就是說,操作前必須找到父節點(通過parentNode來找)

  接下來說下複制節點的方法:

  cloneNode();複制節點,接受一個參數 true或者false。如果true就是複制那個節點和它的子節點。如果是false,就是複制節點本身(複制出來的節點就會沒有任何子元素)。這個方法傳回複制的節點,如果需要操作它,那麼需要借助前面講的4個方法來把這個節點放入到html中去。

  至此,Node類型的常見屬性和方法都介紹完了。結合開頭講的,所有節點類型都繼承自Node類型,是以這些方法是所有節點都有的。

  三、Document類型

  最開始講DOM是什麼的時候提到了Document類型。其實關于這個類型最重要的是它的一個子類HTMLDocument有一個執行個體對象document。而這個document對象是我們最常用的一個對象了。

  document對象又挂載在window對象上,是以在浏覽器就可以直接通路document了。

  老規矩,先講講document對象的屬性,等會講講它的方法。

  document對象上的一些屬性

  document.childNodes 繼承自上面講的Node類型,可以傳回文檔的直接子節點(通常包括文檔聲明和html節點)

  document.documentElement 可以直接拿到html節點的引用(等價于document.getElementsByTagName("html")[0])。

  document.body body節點的引用

  document.title  頁面的title,可以修改,會改變浏覽器标簽上的名字

  document.URL 頁面的url

  document.referrer 取得referrer,也就是打開這個頁面的那個頁面的位址,做來源統計時候比較有用

  document.domain 取得域名,可以設定,但是通常隻能設定為不包含子域名的情況,在一些子域名跨域情況下有效。

  接下來介紹兩個熟悉的方法

  getElementById 和 getElementsByTagName

  getElementById,傳入id,得到元素節點。裡面的參數區分大小寫(IE8-不區分)。注意:如果有多個id相同的元素,則傳回第一個。IE7-裡面表單元素的name也會被當做id來使用。

  getElementsByTagName 根據标簽取得元素,得到的是HTMLCollection類型。如果傳入的是 "*" ,則可以取得全部元素。

  還有一個是隻有HTMLDocument類型(也就是document對象)才有的方法 getElementsByName 顧名思義,根據name傳回元素。

  document對象還有一些集合,例如document.forms 可以傳回所有的form表單。類型也是HTMLCollection。

  說到HTMLCollection,就再說說它

  HTMLCollection就是一個包含一個或多個元素的集合,和上面講的NodeList還挺像的。HTMLCollection這個類型有兩個方法,一個是通過下标(或者.item())得到具體元素,還有就是通過['name'](或者.namedItem())獲得具體元素。

  最後,關于document對象還有一套重要的方法,那便是

  write() writeln() open() close()

  open和close分别是打開和關閉網頁的輸出流,在頁面加載過程中,就相當于open狀态。這兩個方法一般不會去用它。

  然後重要的方法就是write和writeln,它們都是向頁面寫入東西,差別就是後者會多加入一個換行符。

  注意的是:在頁面加載的過程中,可以使用這兩個方法向頁面添加内容。如果頁面已經加載完了,再調用write,會重寫整個頁面。

  還有一點,如果要動态寫入腳本 例如 <script>xxx</script>這樣的 ,那麼要注意把</script>分開來拼裝下,否則會被誤以為是腳本結束的标志,導緻這個結束符比對到上面一個開始符。可以這樣寫"<scr" + "ipt>";

  四、Element類型

  接下來講講最重要也是最常見的一個類型,Element類型。

  我們日常所操作的都是Element類型(實質是HTMLElement,這裡為了友善了解,就簡單這麼說),比如

document.getElementById("test")      

  傳回的就是Element類型。我們日常所說的“DOM對象”,通常也就是指Element類型的對象。

  然後說說這個類型的常見屬性:

  首先最開始說的Node類型上的那些屬性方法它都有,這個就不再重複了,主要說說它自己獨有的。

  首先是tagName,這個和繼承自Node類型的nodeName一樣。都是傳回标簽名,通常是大寫,結果取決于浏覽器。是以在做比較

  的時候最好是調用下類似toLowerCase()這種方法再做比較。

  說說上面提到過的HTMLElement類型

  HTMLElement類型繼承自Element類型,也是HTML元素的實際類型,我們在浏覽器裡用的元素都是這個類型。

  這個類型都具有一些标準屬性,比如:

  id 元素的唯一辨別

  title 通常是滑鼠移上去時候會顯示的資訊

  className 類名

  等等,這幾個屬性是可讀寫的,也就是說你改變他們會得到相應的效果。

  除了屬性外,還有幾個重要的方法

  首先說說操作節點屬性的方法

  getAttribute 、setAttribute 、removeAttribute這3個方法。

  這些是操作屬性最常用的方法了,怎麼用就不說了,很簡單,顧名思義。

  還有一個attributes屬性,儲存了元素的全部屬性。

  這裡停下來,出個問題,ele.className 和 ele.getAttribute("class")傳回的結果是不是同一個東西?

  解答這個問題,我要說一個重要知識點,一個元素的屬性結構是這麼來的,比如一個inpnt元素

  那麼這個元素的屬性被包含在 input.attributes裡面,比如你在html元素上看到的class、id或者你自己定義的data-test這種屬性。

  然後 getAttribute 、setAttribute 、removeAttribute這3個方法可以認為是快捷的取attributes集合的方法。而直接input.id或者input.className都是直接挂在input下的屬性,和attributes是同級的。是以傳回的東西也許看過去一樣,實際是不一樣的,不信你可以試試input.checked這input.getAttribute("checked")試試。

  關于這個知識點,詳細的說可以再寫一篇文章,在我的部落格 ​​從is(":checked")說起​​ 中有談到過,大家可以看看這篇文章和文章後的讨論,便可以知道是怎麼一回事。

  總得來說,這3個方法通常用了處理自定義的屬性,而不是id、class等這種“公認特性”。

  接下來說說建立元素

  document.createElement()可以建立一個元素,比如:

document.createElement("div");      

  一般之後可以為元素設定屬性,兩種方法,一種是直接node.property還可以node.setAttribute("propertyName","value")。等

  但是做完這些之後,這個元素還是沒有在頁面中,是以你還得通過最上面講的類似appendChild這些方法把元素添加到頁面裡面。

  在IE中,還可以直接穿整個HTML字元串進去,來建立元素,比如

document.createElement("test");      

  最後,元素節點也支援HTMLDocument類型的那些查找方法,比如getElementsByTagName。不過它隻會找自己後代的節點。是以可以這麼寫代碼

document.getElementById("test").getElementsByTagName("div");    // 找到id為test元素下的所有div節點      

  五、Text類型

  這個類型很特殊,也是第三常見類型(第一第二分别就是Document和Element)。

  這個節點簡單來說就是一段字元串。

  有個很重要的特征就是,它沒有子元素(不過這個仔細想想也知道= =)

  通路text節點的文本内容,可以通過nodeValue或者data屬性。

  下面簡單說說它提供的一些方法

appendData();    // 在text末尾加内容
deleteData(offset, count);    // 從offset指定的位置開始删除count個字元      

  還有insertDate、replaceData、splitText等方法,就不一一說了,用的機會很少,可以用的時候再查閱。

  然後它還有一個lenght屬性,傳回字元長度的。

  這裡說一個常見的坑。比如下面這個html結構

  這裡,ul的第一個子節點(firstChild)是什麼呢?第一眼看過去,肯定認為是li了,但是實際上,你會發現不是li,而是一個文本節點!

  這是因為浏覽器認為ul和第一個li之間有空白字元,是以就有文本節點了。

  這裡一個常見的問題就是周遊ul的childNodes的時候,周遊的元素一定要判斷下nodeType是不是等于1(等于1就代表是元素節點),這樣才能跳過這個坑。否則你也可以删除所有的空格和換行符。

  建立文本節點的方法是document.createTextNode

  然後接下來和操作Element類型一樣,就是再插入到元素中,浏覽器就可以看到了。

  六、其他的一些類型 Comment、DocumentType和DocumentFragment

  這些不常用的一句話帶過把

  Comment是注釋節點

  DocumentType就是doctype節點,通過docment.doctype來通路

  DocumentFragment這個節點是一個文檔片段,偶爾會用到。

  比如一種常見的用法是,在一個ul中插入3個li。

  如果你循環插入3次,那麼浏覽器就要渲染3次,對性能有蠻大的影響。

  是以大家一般這麼做

  先

var fragment = document.createDocumentFragment();      

  然後循環把li,用appendChild插入到fragment裡面

  最後在一次把fragment插入到ul裡面。這樣就會很快。

  七、DOM擴充

  進過上面講的這麼多節點類型,想必大家對DOM節點已經有了很深的了解,下面講一講DOM擴充的一些東西。

  浏覽器為了友善開發者,擴充了一些DOM功能。

  因為是浏覽器自己擴充的,是以使用前相容性問題一定要注意

  判斷“标準模式”和“混雜模式”通過 document.compatMode和新的document.documentMode

  上面不是說了一個文本節點作為第一子元素的坑嗎,是以浏覽器又實作了一個children屬性,這個屬性隻包含元素節點。

  為了友善判斷A節點是不是B節點的子節點,引入了contains方法,比如 

B.contains(A);    // true就代表是,false就代表不是      

  這個方法有相容性問題,使用前可以谷歌解決方法。

  針對通路元素,又提供了4個方法innerText/innerHTML/outerTEXT/outerHTML。

  通過這些方法,可以讀和寫元素。

  其中,*TEXT是傳回文本内容 *HTML是傳回html文本。

  而outer*則是代表是否包含元素本身。

  實際使用來看,在讀内容的時候 inner*和outer*沒有差別。

  在把内容寫入元素的時候,就是是否包含元素本身的差別。

  重要的是,這幾個方法有性能問題,比如在IE中,通過inner*删除的節點,其綁定的事件依然在記憶體中,就很容易消耗大量記憶體。

  還有一個技巧是,插入大量的html代碼,用innerHTML是非常快的,建議使用。

  八、總結