從原型鏈看DOM--Node類型

前言:html

本系列從原型,原型鏈,屬性類型等方面下手學習了DOM文檔對象模型,旨在弄清咱們在DOM中經常使用的每個屬性和方法都清楚它從哪裏來要到哪裏作什麼事,這樣對於理解代碼有必定啓發。全靠本身在總結中摸索,因此對於一個問題要是深究還真能挖出許多有意思的東西,如今以爲JavaScript這個東西簡直愈來愈有意思了。node

 

正文:
DOM(文檔對象模型)是針對HTML和XML文檔的一個API(應用程序編程接口)。DOM描繪了一個層次化的節點樹,容許開發人員添加,移除,修改頁面某一部分,如今它已成爲表現和操做頁面標記的真正的跨平臺,語言中立的方式。
DOM1爲基本文檔結構及查詢提供了接口。本系列主要討論JavaScript對DOM1級的實現,可是還會穿插一點DOM2和DOM3的內容。編程

 

節點層次數組

DOM能夠將任何HTML和XML文檔描繪成一個由多層節點構成的結構。節點分爲好幾種不一樣的類型,每種類型分別表示文檔中不一樣的信息及標記。每一個節點都有各自特色,屬性和方法,及和其餘節點存在的關係。節點之間的關係構成了層次,而全部頁面標記則表現爲一個以特定節點爲根節點的樹形結構。節點比元素的概念大,元素只是節點的一種類型。瀏覽器

文檔節點:每一個文檔的根節點。HTML文檔中文檔節點( window.document=>#document )只有一個子節點即 HTML 元素。緩存

文檔元素:文檔中最外層元素,文檔中的其餘元素都包含在文檔元素中,每一個文檔只能有一個文檔元素。在HTML頁面中,文檔元素始終都是 HTML 元素。XML中,沒有預約義的元素,任何元素都能成爲文檔元素。app

每一段標記均可經過樹中一個節點表示:HTML元素經過元素節點表示,特性經過特性節點表示,文檔類型經過文檔類型節點表示,註釋經過註釋節點表示...共有12種節點類型,這些類型都繼承自一個基類型Node類型。下面來挨個看這些節點類型,可是本篇着重學習Node類型,其餘類型和DOM操做技術在後續系列的總結中。函數

  1. Node類型
  2. Document類型
  3. Element類型
  4. Text類型
  5. Comment類型
  6. CDATASection類型
  7. DocumentType類型
  8. DocumentFragment類型
  9. Attr類型

DOM操做技術工具

  1. 動態腳本
  2. 動態樣式
  3. 操做表格
  4. 使用NodeList




Node類型:
學習

DOM1級定義了一個Node接口,該接口將由DOM中全部節點類型實現。這個Node接口在JavaScript中做爲Node類型實現,JavaScript中全部節點類型(Element,Attr,Text,CDATASection,Comment,Document,DocumentType,DocumentFragment等)都繼承自Node類型( Element.prototype instanceof Node;// true ),所以全部節點類型的實例都共享着原型鏈(某類型實例->某類型.prototype->Node.prototype->EventTarget.prototype->Object.prototype)上的屬性和方法, document instanceof Node;// true 好比文檔節點 #document 就是Document類型實例也是Node類型的實例,文檔元素 html 是HTMLElement類型的實例是Element類型實例也是Node類型的實例。

下面這段能夠略過,只是個人一個小思考:

document.documentElement.__proto__==HTMLElement.prototype;// false
//竟然是false,這個html元素節點實例的原型指向的不是HTMLElement構造函數的prototype??可是...

HTMLElement.prototype.isPrototypeOf(document.documentElement);// true
//判斷HTMLElement.prototype確實是在html元素的原型鏈上啊

有沒有以爲很奇怪, __proto__ 不是指向構造這個實例的函數的原型嗎??爲何會是 false ,並且 document.documentElement.__proto__ 和 HTMLelement.prototype 居然不是同一類型的,按理說 html 元素做爲HTMLElement的實例,默認它們指向同一個 HTMLElement.prototype 對象的。

百思不得其解,一度認爲我對 __proto__ 這個東西是否是理解有誤,查看相關文檔MDN Object.prototype.__proto__後受了點啓發,

是否是 document.documentElement.__proto__ 被JS引擎重寫了!!?讓其從新指向一個爲HTMLElement類型的實例對象(假設就叫 HTMLEleObj 吧,實際上是 HTMLHtmlElement.prototype ),查看 HTMLEleObj 的 __proto__ ,發現這個東西類型爲Element類型實例,想到 HTMLElement.prototype 也是Element類型的,那這兩個是否是同一個對象?

好像是這樣的: document.documentElement.__proto__.__proto__==HTMLElement.prototype;// true 。這也就能解釋爲何html元素改變了 [[prototype]] 指向但還仍在原來的原型鏈上,JS引擎是給這個自己默認的原型鏈( html.__proto__->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype )又增長了一個對象,如今原型鏈變成( html.__proto__->HTMLEleObj;HTMLEleObj.__proto__->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype )。仍是畫個圖比較好理解點。可是仍是不明白爲什麼JS引擎要在原型鏈上增長這麼個對象,有什麼用處?發現html元素身上有兩個屬性,版本和構造器,然而並不能直接經過 HTMLHtmlElement.prototype.version 訪問 version 屬性(每一個元素的 __proto__ 除了 constructor 屬性外其他的這些屬性還都不同,不過都是HTML5以前元素上的屬性),須要經過它的實例(html元素)訪問。 constructor 指向 HTMLHtmlElement 接口。

---補充---
經過 document.documentElement.__proto__.constructor 訪問獲得,HTMLEleObj對象實際上是HTMLHtmlElement接口的原型對象,雖然以上思考有些誤解,可是仍是留下這個思考的過程吧。真正的原型鏈是  某元素.__proto__->HTML某元素Element.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype  。好比對於html元素就是 document.documentElement.__proto__->HTMLHtmlElement.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype 。對於body元素就是 document.body.__proto__->HTMLBodyElement.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype->Object.prototype 。其實都是能夠經過 某元素.__proto__.constructor 屬性訪問到其對應的構造器。

扯遠了,迴歸主題來看Node類型:

Node.prototype上的屬性及方法

注意到 Node.prototype 是 EventTarget 類型的實例對象,怪不得 Node.prototype.__proto__ 會指向 EventTarget.prototype 。

Object.getOwnPropertyNames(EventTarget.prototype);// 
["addEventListener", "removeEventListener", "dispatchEvent", "constructor"]

全部這些關係總結來講就是Node和EventTarget是JavaScript中兩種類型,用組合繼承模式實現的話就內部實現多是這樣子:

function EventTarget(){
  //初始化實例的屬性和方法
}

function Node(){
 //初始化實例的屬性和方法
}

Node.prototype=new EventTarget();
Node.prototype.construct=Node;

//以Element類型舉例 Element.prototype
=new Node(); Element.construct=Element; ...

 Node.prototype 指向 EventTarget.prototype ,而且 Node.prototype 會被初始化上一些屬性和實例。不過事實上咱們並不能成功構造 Node 和 EventTarget 類型的實例,控制檯會提示報錯不合法的構造。那應該是JS引擎內部本身實現的吧,否則誰都能構造這種底層接口的實例那就亂套了。

 

Node.prototype常見屬性和方法:

這些關係指針屬性大部分都是隻讀的由於訪問器屬性的 set 被設置爲 undefiend 了,即便 set 不爲 undefiend ,但 set 方法能被使用的前提是該節點的對應要訪問的那個屬性不爲 null 。因此DOM提供了一些操做節點的方法(從第9小點開始總結,這些方法都是可寫的,而且在Node.prototype上能夠重寫這些方法)

  1. nodeType屬性:

    每一個節點(Node原型鏈上的實例對象)均可繼承該屬性(經過 Node.prototype 原型鏈訪問),用於代表節點類型。
    節點類型:由在Node類型中定義的下列12個數值常量表示,任何節點類型必是其一。這些類型屬性是定義在Node構造函數身上的(靜態屬性),能夠看到上面的圖有輸出Node上面的屬性。加*爲重點講解。
    * Node.ELEMENT_NODE; 1
    * Node.ATTRIBUTE_NODE; 2
    * Node.TEXT_NODE; 3
    * Node.CDATA_SECTION_NODE; 4
       Node.ENTITY_REFERENCE_NODE; 5
       Node.ENTITY_NODE; 6
       Node.PROCESSING_INSTRUCTION_NODE; 7
    * Node.COMMENT_NODE; 8
    * Node.DOCUMENT_NODE; 9
    * Node.DOCUMENT_TYPE_NODE; 10
    * Node.DOCUMENT_FRAGMENT_NODE; 11
       Node.NOTATON_NODE; 12
    但這些屬性值表示的具體什麼類型節點也可經過直接在 Node.prototype 對象上訪問或經過原型鏈訪問。好比要訪問註釋Comment類型節點,三種方式都可
    Node.prototype.COMMENT_NODE==Node.COMMENT_NODE;// true 
    Node.prototype.COMMENT_NODE;// 8
    Node.COMMENT_NODE;// 8
    document.COMMENT_NODE;// 8
    應用:經過利用節點類型屬性能夠肯定節點的類型,爲了兼容那些沒有公開Node類型的構造函數的瀏覽器,咱們就不用Node.ELEMENT_NODE形式訪問類型值,而是直接經過數字值判斷
    if(someNode.nodeType==1){
       console.log("這是一個元素節點");
    }
  2.  nodeName和nodeValue屬性:

    表示節點具體信息。
    (1).對於Element元素節點:原型鏈繼承關係爲:某元素節點.__proto__->HTML某元素Element.prototype->HTMLElement.prototype->Element.prototype->Node.prototype->EventTarget.prototype。

    nodeName保存的爲元素的標籤名,nodeValue的值爲null。

    var someNode=document.documentElement;// 獲取HTML元素
    if(someNode.nodeType==1){
      console.log(someNode.nodeName+"是元素節點名");
      console.log("它的nodeValue:"+someNode.nodeValue);
    }
    /*HTML是元素節點名
       它的nodeValue:null */
    (2).對於Attr類型節點:原型鏈繼承關係爲:某特性節點.__proto__->Attr.prototype->Node.prototype->EventTarget.prototype。

    nodeName值是特性的名稱,nodeValue值是特性的值。
    var html=document.documentElement;
    //獲取特性實例所在的對象
    html.attributes;//attributes屬性是Element.prototype上的屬性

     
    這個對象是NamedNodeMap類型的實例,這個對象的原型鏈關係爲html.attributes.__proto__->NamedNodeMap.prototype->Object.prototype。這個對象裏面又有幾個屬性,這幾個屬性纔是咱們須要的真正特性對象。

    html.attributes["0"];// lang="zh-cn"  是一個特性節點
    html.attributes["1"];// style="overflow:hidden;" 是另外一個特性節點
    
    html.attributes["lang"].nodeName;// "lang" lang特性節點的nodeName值
    html.attributes["lang"].nodeValue;//  "zh-cn"
    
    html.attributes["0"] instanceof Attr;// true
    (3).對於Text類型節點:原型鏈的繼承關係爲:某文本節點.__proto__->Text.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。

    nodeName值爲"#text",nodeValue值爲節點所包含的文本。
    (4).對於CDATASection類型節點:該類型只針對基於XML文檔。原型鏈的繼承關係爲:CDATASection實例._proto__->CDATASection.prototype->Text.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。

    nodeName值爲"#cdata-section",nodeValue值爲CDATA區域中的內容。
    (5).對於Comment類型節點:原型鏈的繼承關係爲:Comment類型實例.__proto__->Comment.prototype->CharacterData.prototype->Node.prototype->EventTarget.prototype。

    nodeName值爲"#comment",nodeValue值爲註釋的內容。
    (6).對於Document類型節點:原型鏈的繼承關係爲(以瀏覽器中document爲例):document.__proto__->HTMLDocument.prototype->Document.prototype->Node.prototype->EventTarget.prototype。

    能夠看到Document.prototype上的屬性和方法不少有176個,返回這個數組是Array類型的實例。







    發現了document節點對象和element元素節點對象的事件屬性還不是統一繼承的,是在各自原型鏈上繼承的事件屬性。
    NodeName值爲"#document",NodeValue值爲null。
    (7).對於DocumentType類型節點:原型鏈的繼承關係爲:document.doctype.__proto__->DocumentType.prototype->Node.prototype->EventTarget.prototype。

    nodeName的值爲doctype的名稱,nodeValue的值爲null。
    (8).對於DocumentFragment類型的節點:原型鏈的繼承關係爲:文檔片斷實例.__proto__->DocumentFragment.prototype->Node.prototype->EventTarget.prototype。

    注意到這也和Document.prototype上的方法重合了。
    nodeName的值"#document-fragment",nodeValue的值爲null。
  3. childNodes屬性:

    每一個節點均可繼承該屬性,其中保存着NodeList對象。
    區別NodeList接口和HTMLCollection接口:
    (1).NodeList接口是爲節點的childNodes屬性提供的,原型鏈的關係爲:某節點.childNodes.__proto__->NodeList.prototype->Object.prototype。
    符合的有getElementsByName,childNodes,querySelectorAll等

    對於原型上的item方法,返回NodeList對象中指定索引的節點,若是索引越界,則返回 null 。等價的寫法是 nodeList[idx] , 不過這種狀況下越界訪問將返回 undefined (由於是以數組形式訪問的)。
    對arguments對象使用Array.prototype.slice()方法將其轉換爲數組,採用一樣方法也能夠將NodeList類型集合轉換爲數組類型,其實就是就是在類數組對象的上下文中調用原生的slice方法。
    function transToArr(collections,start,end){
       var length=arguments.length;
       if(length==0){
           return;
       }else if(length==1){
           return Array.prototype.slice.call(collections); 
       }else{
       //判斷start,end類型
       if(typeof arguments[1]=='number'){
           if(typeof arguments[2]=='number'){
               return Array.prototype.slice.call(collections,start,end);
           }else{
              //end參數不是number類型時,slice返回length以前的項
              end=collections.length;
              return Array.prototype.slice.call(collections,start,end);
           }
        }
      }
    }
    (2)HTMLCollection接口是爲一個包含了元素的通用集合,原型鏈的關係爲:經過某用法(好比getElementsByTagName,getElementsByClassName,getElementsByTagNameNS,document.forms等)獲取的節點集合.__proto__->HTMLCollection.prototype->Object.prototype

    (3)NodeList類型集合大部分時候和HTMLCollection類型集合都是即時更新的,當其所包含的文檔結構發生改變時,它會自動更新。如下圖片爲示例,nodechilds集合和lis集合雖然保存的內容同樣,但它兩不相等,是由於nodechilds保存的引用地址和lis保存的引用地址不同,但這兩引用地址所指向的內存堆中各的自集合對象裏的每一項引用都是同一個element節點對象。因此刪除一個li節點後,nodechilds和lis集合都會發生變化。


    正是由於動態集合,childNodes.length會實時變化,於是
    //刪除childNodes中的全部文本節點,由於child.length是動態變化的,因此分狀況i++
    var child=parent.childNodes;
    for(var i=0;i<child.length;){
      if(child[i].nodeType==3){
         ul.removeChild(child[i]); //不用i=0迴歸到開時就用上次的i就可    
      }else{
         i++;
      }
    }
    但NodeList也有時候表現爲靜態集合,以意味着對文檔對象模型任何改動都不會影響集合內容。querySelectorAll就是靜態的

    因此當你選擇遍歷NodeList中全部項,或緩存列表長度時候,考慮要用哪一種。
  4. parentNode屬性:

    該屬性指向文檔樹中的父節點(多是Document類型也多是Element類型)
  5. previousSibling和nextSibling屬性:

    childNodes列表中第一個節點的previousSibling屬性值爲null,列表中最後一個節點的nextSibling屬性也爲null。
  6. firstChild和lastChild屬性:

    父節點的這兩個屬性分別指向其childNodes的第一個和最後一個節點,在只有一個子節點狀況下,firstChild和lastChild指向同一個節點。若沒有子節點,這兩屬性爲null。
  7. hasChildNodes():

    這個函數的writable屬性看來是容許可寫的,那就重寫該方法試試,我是重寫在節點實例對象上了,固然你也能夠在原型Node.prototype上重寫該方法是能夠的。

    在節點包含一個或多個子節點狀況下返回true,在判斷時這比childNodes.length更簡便。
  8. ownerDocument:

    該屬性指向表示整個文檔的文檔節點。這種關係表示的是任何節點都屬於它所在的文檔,任何節點都不能同時存在兩個或兩個以上文檔中。當咱們沒必要在節點層次中經過層層回溯到達頂端而是能夠直接 節點對象.ownerDocument 訪問,但要注意 document.ownerDocument;// null 文檔節點自己的文檔節點爲null。
  9. parentNode.appendChild():返回新增節點。
    又是偶然我發現一個好玩的現象,當重寫Node.prototype.appendChild方法後,發現只要可得到焦點的區域(好比a元素,input元素,button元素等)得到焦點後就會執行appendChild函數,可是當輸入內容期間並不觸發該函數,而後刪除內容的時候又會觸發該函數執行。每次執行就執行吧它還很奇怪的執行4次。由此能夠猜測當在文檔中得到一個焦點後就至關於觸發了appendChild事件??由於DOM文檔結構原本靜態的,忽然插進來一個光標,DOM結構被改動了因此纔會觸發appendChild?由於光標一直在DOM結構中的某個位置若是沒有移出的話,在原地方編輯內容由於仍是在原地方改動因此並不觸發appendChild。就是不理解爲何每次觸發要執行4次該函數(對於博客園這個編輯頁面來講是4次,其餘頁面測試也有3次的)。而咱們日常之因此插入光標,輸入內容,刪除內容,離開光標感受瀏覽器對此並沒什麼反應我估計是JS引擎實現appendChild方法內部給作了妥善處理,其實應該仍是觸發這個DOM級的事件了吧。

    再次迴歸主題:appendChild()用於向childNodes列表的末尾添加一個節點,添加後,childNodes的新增節點,父節點及之前的最後一個子節點的關係指針都會獲得相應更新。若是傳入到appendChild()中的節點已是文檔中的一部分了,那結果就是將該節點從原來位置移動到新位置。
    var a=document.body.firstChild;
    document.body.appendChild(document.body.firstChild)==a;// true
    a==document.body.lastChild;// true
  10. parentNode.insertBefore(要插,參照):插入到childNodes列表中某個特定位置,參數一爲要插入的節點,參數二爲做爲參照的節點。插入節點後被插入的節點會變成參照節點的前一個同胞節點(previousSibling),返回被插入的節點。若是參照節點是null,則insertBefore的效果和appendChild()同樣,能夠這麼理解:把源節點插入到目標節點null以前,那誰是null節點呢,不就是父節點的最後一個子節點的nextSibling嗎,因此插到這個null節點以前就至關於插入父節點的最後一個子節點以後。基於這個思路能夠實現咱們本身的insertAfter()
    //要插入到desEle以後,至關於插入desEle.nextSibling以前,返回被插入的srcEle
    Node.prototype.insertAfter=function(srcEle,desEle){
        this.insertBefore(srcEle,desEle.nextSibling);
        return srcEle;
    }
  11. parentNode.replaceChild(newNode,parentNode.child):參數爲要插入的新節點,要替換的節點。要替換的節點將由這個方法返回並從文檔樹中移除,要插入的節點佔據其位置。新節點的全部關係指針都會從被它替換的節點複製過來。儘管從技術上講被替換的節點仍然還在文檔中,但它在文檔中已經沒有本身的位置。
    var parent=$('#hdtb-msb');
    var first=parent.firstChild;
    var last=parent.lastChild;
    var firstnode=parent.replaceChild(last,first);
    firstnode;// <div class=​"hdtb-mitem hdtb-msel hdtb-imb">​所有​</div>​
    firstnode.ownerDocument;// #document 節點 證實還在文檔樹
  12. removeChild():參數爲要移除的節點,返回被移除的節點。被移除的節點仍然爲文檔全部,但在文檔中已經沒有本身的位置。
  13. node.cloneNode():用於建立調用這個方法的節點的一個徹底相同的副本。參數爲true表示進行深複製(複製節點及其整個子節點樹),參數爲false進行淺複製(只複製節點自己)。複製後返回的節點屬於文檔全部但並無爲它指定父節點,可經過appendChild(),insertBefore(),replaceChild()將它添加到文檔中。
    var clone1=last.cloneNode(true);
    clone1;// <a class=​"hdtb-tl" id=​"hdtb-tls" role=​"button" tabindex=​"0" data-ved=​"0ahUKEwiYht2SptbLAhVDao4KHSbJBqIQ2x8ICigF">​搜索工具​</a>​
    var clone2=last.cloneNode(false);
    clone2;// <a class=​"hdtb-tl" id=​"hdtb-tls" role=​"button" tabindex=​"0" data-ved=​"0ahUKEwiYht2SptbLAhVDao4KHSbJBqIQ2x8ICigF">​</a>​
    clone1.ownerDocument==clone2.ownerDocument // true

    cloneNode()方法不會複製添加DOM節點的JavaScript屬性,例如事件處理程序。這個方法只複製特性(包括經過特性綁定的事件處理程序 <h1 onclick="console.log('xxx')">xxx</h1> 會將事件複製成功),子節點(深複製狀況下),其餘一切都不會複製。 

  14. normalize():處理文檔樹中的文本節點。因爲解析器或DOM操做等緣由,可能會出現文本節點不包含文本,或者接連出現兩個文本節點的狀況。當在某個節點上調用這個方法時,就會在該節點的後代節點中查找上述兩種狀況,若是找到空文本節點就刪除它,若是找到相鄰文本節點則將它合併爲一個文本節點。
    空文本節點指的是內容爲空才能被刪除,好比
    document.createTextNode('');
    document.createTextNode(' ');
    document.createTextNode('   ');
    ...

     也就是看該文本節點的data(ChacterData.prototype上的屬性)值就能夠了,圖片上的data值是回車雖然在呈現上和空文本節點同樣,但並非空因此不能被刪除了,因此注意這樣編代碼ul的childNodes裏的文本節點實際上是回車符。

    <ul>
        <li></li>
        <li></li>
    </ul>

 

參考:
《JavaScript高級程序設計》

MDN HTMLCollection
MDN NodeList

相關文章
相關標籤/搜索