先來一顆栗子:javascript
<img src="/sub/123.jpg" alt="test" /> <script type="text/javascript"> var img = document.getElementsByTagName('img')[0]; console.log('src:', img.src); </script>
輸出 src: sub/123.jpg?No,輸出的是 src: http://127.0.0.1:8020/sub/123.jpg, 但我其實只想要一個pathname而已啊。雖然有一萬種辦法能夠從完整地址中取出pathname,但我仍是想一次獲取我們寫到屬性裏面的那個src啊。css
固然,這樣的接口必須是有的:html
console.log('src:', img.getAttribute('src'));
// src: /sub/123.jpg
之前總以爲HTML裏面東西很少,但如今發現實際上是本身瞭解的很少,仍是too naive啊!DOM結構竟然都這麼不熟悉。找到問題,又回去看了看高程3,總以爲裏面按照DOM123分類很沒邏輯,仍是試着按照功能區分總結一下吧。java
先看看DOM結構層次,上個栗子:node
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>DOM</title> </head> <body> <!--wrapper--> <div class="wrp"> wrp: <img src="/sub/123.jpg" alt="test" /> <script type="text/javascript"> var img = document.getElementsByTagName('img')[0]; console.log('src:', img.getAttribute('src')); </script> </div> </body> </html>
這種比較常見的HTML文本,裏面就已經包含了咱們經常使用到的一個Node類型。DOM會將HTML文檔以節點樹的形式組織起來,也就是說Node與HTML文檔是對應,即便是代碼中的換行這種美觀上東西,也會真實地反映到節點樹上。chrome
上面的HTML對應的Nodelist:編程
整個文檔就是一個document節點,這個咱們用的也比較多了。document中有幾個屬性用的比較少:URL、domain、referrer,其中domain能夠設置,利用這一點能夠解決一些子域之間的跨域問題。另外的一些特殊集合,如forms、images、links這些,能夠方便操做,作爬蟲的時候也能夠用來簡化模型。跨域
document下面有document type、element、comment、text幾種經常使用的節點。數組
這裏要注意的是Text節點和element節點的區別,一般咱們會在element節點裏面寫入字符,但並非說element裏面包含字符,而是element節點裏面嵌入了一個text節點,text節點裏面的內容纔是咱們寫進的字符。例如 <p>papapa</p>的結構應該是:app
> 換行
另一個常被忽略的是換行。爲了美化代碼,一般咱們每一個標籤之間都會換行,DOM在解析HTML文檔時,會把換行也解析成 Textnode 節點!也就是咱們每一行HTML代碼後面都自帶一個textnode節點\n!
說到關係,也就是父子關係、兄弟關係這兩種了。爲了定位一個節點,咱們能夠須要DOM提供的幾個定位接口:firstChild、lastChild、previousSibling、nextSibling,還有parentNode、childNodes。利用這幾個接口的組合就能夠定位到具體某個節點了。
還有一點就是Nodelist自己,Nodelist是一個動態的類數組對象,動態的意思就是DOM發生改變以後,變更會實時更新到Nodelist上,從這個性質來看,Nodelist應該是一個引用集或者指針集。類數組的意思就是說其實人家不是真正的數組,只是長得有點像。因此遍歷的時候用for in也會將某些諸如對象自己的方法屬性都遍歷出來啦,仍是常規的for i to length就行了,也能夠用forEach、for of啦。
每一個node節點都有相應的nodetype、nodeName和nodeValue屬性,見名知義,分別表明了節點的類型、名字和值。這裏要說一下,element節點的nodeName爲標籤名,而nodeValue則爲null,文本和comment的nodeValue爲相應的字符串。
說到操做,無非就是增刪改查。而DOM操做中,主體都是element節點,因此基本也是對element節點進行操做。
DOM的查有兩種:準確查詢和遍歷查詢。
1) 準確查詢是給定一個條件去搜索,主要有兩類接口:getElementBy***與querySelector***。
這兩種查詢的區別在於實時性與非實時性。getElement**的方式返回的是Nodelist,因此具備實時性。而querySelector**返回的是一個快照,DOM表的變化不能實時反映到查詢的結果裏面。看看這個例子:
<div class="wrp"> <p>papapa</p> </div> <script> var divs = document.getElementsByClassName('wrp'), // 複製7個 // var divs = document.querySelectorAll('.wrp'), // 複製1個 i, div = null; for(i = 0; i < divs.length; i++) { div = divs[0].cloneNode(true); document.body.appendChild(div); if(i > 5) { break; } } </script>
採用getElements***的方式,那divs是動態變化的,因此會複製7個papapa出來。假如採用querySelector***的方式,則divs只保存了剛查詢的時候那個狀態,所以只會複製一個papapa出來。因此,假如採用getElement的方式查詢,那麼就須要採用一個len變量來保存當前狀態,不然就會形成死循環。按照咱們日常的認知思惟,divs也不該該動態變化,畢竟我後面沒有繼續查詢啊,你怎麼能變呢。因此,querySelector的方式更可控,不會搞出一些莫名其妙的bug,並且jQuery風格想必你們仍是喜歡的。
2)遍歷
DOM提供了2個遍歷迭代器:NodeIterator與TreeWalker,固然你也能夠手動實現遍歷。
NodeIterator的用法看下面例子:
1 <div class="wrp"> 2 <p>papapa</p> 3 <div> 4 <span>yohohoho</span> 5 <input class='input1' type="text" name="" value="" /> 6 </div> 7 <input class="input2" type="text" name="" value="" /> 8 </div> 9 <script> 10 var div = document.querySelector('.wrp'); 11 var filter = function(node) { 12 return node.tagName.toLowerCase() === 'input' ? 13 NodeFilter.FILTER_ACCEPT : 14 NodeFilter.FILTER_SKIP; 15 } 16 var myIterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false); 17 var n = myIterator.nextNode(); // move to root(div) 18 while(n !== null) { 19 console.log(n.className); 20 n = myIterator.nextNode(); 21 } 22 </script>
TreeWalker遍歷:
1 var div = document.querySelector('.wrp'); 2 var filter = function(node) { 3 return node.tagName.toLowerCase() === 'input' ? 4 NodeFilter.FILTER_ACCEPT : 5 NodeFilter.FILTER_SKIP; 6 } 7 8 var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false); 9 var n = walker.firstChild(); 10 while(n !== null) { 11 console.log(n.className); 12 n = walker.nextNode(); 13 }
手動方式,先說說思路:首先要考慮子級中還有子級,那顯然這裏要用遞歸的形式來作,判斷節點是否有子節點而遞歸。而後就是一些細節方面的,好比爲了剔除換行符和文本節點的影響,那固然是直接使用children來獲取子級節點了。另外還須要避免Nodelist的動態變化之類的。看代碼:
1 function mywalker(el, filter, cb) { 2 var children = el.children; 3 for(var i = 0, len = children.length; i < len; i++) { 4 var walker = children[i]; 5 if(walker.children.length !== 0) { 6 mywalker(walker, filter, cb); 7 } else if(filter(walker)) { 8 cb(walker); 9 } 10 } 11 } 12 13 var div = document.querySelector('.wrp'); 14 15 function filter(node) { 16 return node.tagName.toLowerCase() === 'input'; 17 } 18 19 function cb(node) { 20 console.log(node.className); 21 } 22 23 mywalker(div, filter, cb);
若是還要考慮文本節點的話,那可使用childNodes來遍歷,這個時候就要注意過濾換行符了。
DOM節點的增接口有:appendChild() 和 insertBefore()。但建立節點則須要用到另外兩個接口:document.creat***方式和document.cloneNode()。建立節點以後經過添加接口將節點放入文檔中。這裏除了能夠建立普通的節點以外,也能夠建立腳本和樣式表,這種用法能夠實現按需加載,延遲加載等各類資源加載方法。固然要作成像requirejs那樣的加載器,那就須要添加不少處理邏輯了。
刪除有兩個接口:replaceChild、removeChild。
1)內容的更改:innerHTML、innerText,textContent之類的。textContent通常不會用到,他是將節點內部全部文本節點拼接在一塊兒的字符串,包括換行符這些也都塞了進去,因此還須要進一步進行字符串處理。
2)屬性的更改
文章開頭其實就已經提到了最經常使用的兩種更改屬性的方法,分別是 Element.props=value 和 Element.setAttribute() 系列。其中getAttribute()取的是Attribute節點上的值,也就是對HTML標籤的尖括號<>內的這串字符進行查詢。假如沒定義則返回一個null,有定義的則返回一個字符串。而Element.props是對HTML標籤的實例進行查詢,也就是對一個實例化後的節點對象進行查詢。而這個對象在初始化的時候就會將HTML標籤中沒定義的屬性置爲」」有定義則進行解析轉換。因此對同一個對象進行查詢,沒定義的屬性.prop返回空值而getAttribute返回null;對style和onload一類事件屬性進行查詢,前者返回一個對象,然後者返回一個字符串或null;對自定義屬性的查詢,前者返回一個undefined,然後者則返回自定義的值。
而文章開頭中兩種查詢獲得的結果不一樣,緣由就在於此。
> attributes
節點中統一管理各類屬性的是 attributes 屬性,它是一個NamedNodeMap,保存了對象已定義的全部屬性。這貨其實也是一個類數組,這裏chrome卻是打印的很清楚,一目瞭然。記得不能用for in遍歷哦。
能夠經過attributes[‘id’]的形式去查詢相應的屬性,它自身也有一些方法,but沒什麼人會用它,並且attributes這個屬性自己就不多會被用到。使用它的場景之一(或者能夠說惟一)就是要對屬性的集中管理,或者遍歷或者批量初始化,而日常咱們用的基本都是.prop的形式,直觀方便。
> 自定義屬性
咱們能夠往標籤里加入任意的自定義屬性,自由雖好但規範仍是要有。HTML5中是以data-prop的形式來定義自定義屬性的。掛在data下的屬性就能夠經過dataset屬性統一管理,通常能夠將數據放到data上,而後渲染的時候直接讀取dataset屬性進行渲染了,像knockout這種庫也是這麼來進行數據綁定的。
> 樣式
屬性中很是重要的一點就是樣式,這裏又涉及到內聯樣式和嵌入、外聯兩類的樣式控制。上文也說到,使用element.stylt.樣式能夠直接更改樣式,也能夠經過element.style.cssText來一次讀寫全部的內聯樣式。但樣式比較複雜的地方是嵌入、外聯樣式的疊加影響,因此單純查詢style並不能獲得元素的最終樣式。這個時候能夠經過document.defaultView的getComputedStyle()方法來獲取計算樣式,就像chrome開發者工具那樣獲得計算樣式。
可是,CSS是個大工程,試圖經過js來動態控制全部樣式的變化是不靠譜的。更爲常見的作法是CSS預設各類狀態,而js做爲控制來控制狀態的轉換,也就是控制class的變換。DOM編程中除了單純的className以外,更爲強大的接口是classList,這又是一個類數組對象。它提供了幾個好用的類管理方法:add、contains、remove、toggle,滿滿jQuery風啊。但jQuery畢竟是個民間女子,當正統後宮吸取了她的各類奇法淫技以後,失寵也是不可避免的。
琢磨了很久,但寫出來仍是各類凌亂,結構仍是不夠清晰。DOM的結構層次以Nodelist爲基礎,不一樣nodeType的節點有機組合就構成了一個DOM表。對DOM的操做有各類接口,感受比較混亂,可能這也是各類框架一直致力於弱化開發者對DOM操做的緣由吧,但在超小規模應用上熟悉了DOM操做的話,仍是會比使用框架更加靈活。
對DOM的總結就到這裏了,看了下仍是整理爲主,背書爲輔哈,必須填圖了。